@powersync/service-core 0.0.0-dev-20241016162519 → 0.0.0-dev-20241018075839

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.
@@ -31,6 +31,8 @@ import { BucketDataDocument, BucketDataKey, SourceKey, SyncRuleState } from './m
31
31
  import { MongoBucketBatch } from './MongoBucketBatch.js';
32
32
  import { MongoCompactor } from './MongoCompactor.js';
33
33
  import { BSON_DESERIALIZE_OPTIONS, idPrefixFilter, mapOpEntry, readSingleBatch, serializeLookup } from './util.js';
34
+ import { logger } from '@powersync/lib-services-framework';
35
+ import * as timers from 'timers/promises';
34
36
 
35
37
  export class MongoSyncBucketStorage
36
38
  extends DisposableObserver<SyncRulesBucketStorageListener>
@@ -459,10 +461,28 @@ export class MongoSyncBucketStorage
459
461
  }
460
462
 
461
463
  async clear(): Promise<void> {
464
+ while (true) {
465
+ try {
466
+ await this.clearIteration();
467
+ return;
468
+ } catch (e: unknown) {
469
+ if (e instanceof mongo.MongoServerError && e.codeName == 'MaxTimeMSExpired') {
470
+ logger.info(
471
+ `Clearing took longer than ${db.mongo.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, waiting and triggering another iteration.`
472
+ );
473
+ await timers.setTimeout(db.mongo.MONGO_CLEAR_OPERATION_TIMEOUT_MS / 5);
474
+ continue;
475
+ } else {
476
+ throw e;
477
+ }
478
+ }
479
+ }
480
+ }
481
+
482
+ private async clearIteration(): Promise<void> {
462
483
  // Individual operations here may time out with the maxTimeMS option.
463
484
  // It is expected to still make progress, and continue on the next try.
464
485
 
465
- // TODO: Transactional?
466
486
  await this.db.sync_rules.updateOne(
467
487
  {
468
488
  _id: this.group_id
@@ -476,48 +496,33 @@ export class MongoSyncBucketStorage
476
496
  no_checkpoint_before: null
477
497
  }
478
498
  },
479
- { maxTimeMS: db.mongo.MONGO_OPERATION_TIMEOUT_MS }
499
+ { maxTimeMS: db.mongo.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
480
500
  );
481
501
  await this.db.bucket_data.deleteMany(
482
502
  {
483
503
  _id: idPrefixFilter<BucketDataKey>({ g: this.group_id }, ['b', 'o'])
484
504
  },
485
- { maxTimeMS: db.mongo.MONGO_OPERATION_TIMEOUT_MS }
505
+ { maxTimeMS: db.mongo.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
486
506
  );
487
507
  await this.db.bucket_parameters.deleteMany(
488
508
  {
489
509
  key: idPrefixFilter<SourceKey>({ g: this.group_id }, ['t', 'k'])
490
510
  },
491
- { maxTimeMS: db.mongo.MONGO_OPERATION_TIMEOUT_MS }
511
+ { maxTimeMS: db.mongo.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
492
512
  );
493
513
 
494
514
  await this.db.current_data.deleteMany(
495
515
  {
496
516
  _id: idPrefixFilter<SourceKey>({ g: this.group_id }, ['t', 'k'])
497
517
  },
498
- { maxTimeMS: db.mongo.MONGO_OPERATION_TIMEOUT_MS }
518
+ { maxTimeMS: db.mongo.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
499
519
  );
500
520
 
501
521
  await this.db.source_tables.deleteMany(
502
522
  {
503
523
  group_id: this.group_id
504
524
  },
505
- { maxTimeMS: db.mongo.MONGO_OPERATION_TIMEOUT_MS }
506
- );
507
- }
508
-
509
- async setSnapshotDone(lsn: string): Promise<void> {
510
- await this.db.sync_rules.updateOne(
511
- {
512
- _id: this.group_id
513
- },
514
- {
515
- $set: {
516
- snapshot_done: true,
517
- persisted_lsn: lsn,
518
- last_checkpoint_ts: new Date()
519
- }
520
- }
525
+ { maxTimeMS: db.mongo.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
521
526
  );
522
527
  }
523
528
 
@@ -62,6 +62,9 @@ export class PowerSyncMongo {
62
62
  this.locks = this.db.collection('locks');
63
63
  }
64
64
 
65
+ /**
66
+ * Clear all collections.
67
+ */
65
68
  async clear() {
66
69
  await this.current_data.deleteMany({});
67
70
  await this.bucket_data.deleteMany({});
@@ -73,4 +76,13 @@ export class PowerSyncMongo {
73
76
  await this.instance.deleteOne({});
74
77
  await this.locks.deleteMany({});
75
78
  }
79
+
80
+ /**
81
+ * Drop the entire database.
82
+ *
83
+ * Primarily for tests.
84
+ */
85
+ async drop() {
86
+ await this.db.dropDatabase();
87
+ }
76
88
  }
@@ -1438,4 +1438,26 @@ bucket_definitions:
1438
1438
  expect(errorCaught).true;
1439
1439
  expect(isDisposed).true;
1440
1440
  });
1441
+
1442
+ test('empty storage metrics', async () => {
1443
+ const f = await factory({ dropAll: true });
1444
+
1445
+ const metrics = await f.getStorageMetrics();
1446
+ expect(metrics).toEqual({
1447
+ operations_size_bytes: 0,
1448
+ parameters_size_bytes: 0,
1449
+ replication_size_bytes: 0
1450
+ });
1451
+
1452
+ const r = await f.configureSyncRules('bucket_definitions: {}');
1453
+ const storage = f.getInstance(r.persisted_sync_rules!);
1454
+ await storage.autoActivate();
1455
+
1456
+ const metrics2 = await f.getStorageMetrics();
1457
+ expect(metrics2).toEqual({
1458
+ operations_size_bytes: 0,
1459
+ parameters_size_bytes: 0,
1460
+ replication_size_bytes: 0
1461
+ });
1462
+ });
1441
1463
  }
@@ -5,15 +5,7 @@ import { JSONBig } from '@powersync/service-jsonbig';
5
5
  import { RequestParameters } from '@powersync/service-sync-rules';
6
6
  import * as timers from 'timers/promises';
7
7
  import { describe, expect, test } from 'vitest';
8
- import {
9
- BATCH_OPTIONS,
10
- makeTestTable,
11
- MONGO_STORAGE_FACTORY,
12
- PARSE_OPTIONS,
13
- StorageFactory,
14
- ZERO_LSN
15
- } from './util.js';
16
- import { ParseSyncRulesOptions, StartBatchOptions } from '@/storage/BucketStorage.js';
8
+ import { BATCH_OPTIONS, makeTestTable, MONGO_STORAGE_FACTORY, PARSE_OPTIONS, StorageFactory } from './util.js';
17
9
 
18
10
  describe('sync - mongodb', function () {
19
11
  defineTests(MONGO_STORAGE_FACTORY);
@@ -38,8 +30,7 @@ function defineTests(factory: StorageFactory) {
38
30
  content: BASIC_SYNC_RULES
39
31
  });
40
32
 
41
- const storage = await f.getInstance(syncRules);
42
- await storage.setSnapshotDone(ZERO_LSN);
33
+ const storage = f.getInstance(syncRules);
43
34
  await storage.autoActivate();
44
35
 
45
36
  const result = await storage.startBatch(BATCH_OPTIONS, async (batch) => {
@@ -91,7 +82,6 @@ function defineTests(factory: StorageFactory) {
91
82
  });
92
83
 
93
84
  const storage = await f.getInstance(syncRules);
94
- await storage.setSnapshotDone(ZERO_LSN);
95
85
  await storage.autoActivate();
96
86
 
97
87
  const result = await storage.startBatch(BATCH_OPTIONS, async (batch) => {
@@ -136,7 +126,6 @@ function defineTests(factory: StorageFactory) {
136
126
  });
137
127
 
138
128
  const storage = await f.getInstance(syncRules);
139
- await storage.setSnapshotDone(ZERO_LSN);
140
129
  await storage.autoActivate();
141
130
 
142
131
  const stream = streamResponse({
@@ -164,7 +153,6 @@ function defineTests(factory: StorageFactory) {
164
153
  });
165
154
 
166
155
  const storage = await f.getInstance(syncRules);
167
- await storage.setSnapshotDone(ZERO_LSN);
168
156
  await storage.autoActivate();
169
157
 
170
158
  const stream = streamResponse({
@@ -226,7 +214,6 @@ function defineTests(factory: StorageFactory) {
226
214
  });
227
215
 
228
216
  const storage = await f.getInstance(syncRules);
229
- await storage.setSnapshotDone(ZERO_LSN);
230
217
  await storage.autoActivate();
231
218
 
232
219
  const exp = Date.now() / 1000 + 0.1;
@@ -265,7 +252,6 @@ function defineTests(factory: StorageFactory) {
265
252
  });
266
253
 
267
254
  const storage = await f.getInstance(syncRules);
268
- await storage.setSnapshotDone(ZERO_LSN);
269
255
  await storage.autoActivate();
270
256
 
271
257
  await storage.startBatch(BATCH_OPTIONS, async (batch) => {
package/test/src/util.ts CHANGED
@@ -24,11 +24,22 @@ await Metrics.initialise({
24
24
  });
25
25
  Metrics.getInstance().resetCounters();
26
26
 
27
- export type StorageFactory = () => Promise<BucketStorageFactory>;
27
+ export interface StorageOptions {
28
+ /**
29
+ * By default, collections are only cleared/
30
+ * Setting this to true will drop the collections completely.
31
+ */
32
+ dropAll?: boolean;
33
+ }
34
+ export type StorageFactory = (options?: StorageOptions) => Promise<BucketStorageFactory>;
28
35
 
29
- export const MONGO_STORAGE_FACTORY: StorageFactory = async () => {
36
+ export const MONGO_STORAGE_FACTORY: StorageFactory = async (options?: StorageOptions) => {
30
37
  const db = await connectMongo();
31
- await db.clear();
38
+ if (options?.dropAll) {
39
+ await db.drop();
40
+ } else {
41
+ await db.clear();
42
+ }
32
43
  return new MongoBucketStorage(db, { slot_name_prefix: 'test_' });
33
44
  };
34
45