@powersync/service-module-mongodb-storage 0.0.0-dev-20251111093449 → 0.0.0-dev-20251120150014
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.
- package/CHANGELOG.md +29 -5
- package/dist/storage/MongoReportStorage.js +2 -2
- package/dist/storage/MongoReportStorage.js.map +1 -1
- package/dist/storage/implementation/MongoChecksums.d.ts +7 -1
- package/dist/storage/implementation/MongoChecksums.js +15 -6
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +1 -0
- package/dist/storage/implementation/MongoCompactor.js +40 -10
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js +8 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +4 -0
- package/dist/utils/util.d.ts +0 -1
- package/dist/utils/util.js +9 -15
- package/dist/utils/util.js.map +1 -1
- package/package.json +7 -7
- package/src/storage/MongoReportStorage.ts +2 -2
- package/src/storage/implementation/MongoChecksums.ts +16 -6
- package/src/storage/implementation/MongoCompactor.ts +45 -11
- package/src/storage/implementation/MongoSyncRulesLock.ts +13 -4
- package/src/storage/implementation/models.ts +5 -0
- package/src/utils/util.ts +9 -16
- package/test/src/__snapshots__/connection-report-storage.test.ts.snap +90 -11
- package/test/src/storage_compacting.test.ts +1 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -61,6 +61,7 @@ export interface MongoCompactOptions extends storage.CompactOptions {}
|
|
|
61
61
|
const DEFAULT_CLEAR_BATCH_LIMIT = 5000;
|
|
62
62
|
const DEFAULT_MOVE_BATCH_LIMIT = 2000;
|
|
63
63
|
const DEFAULT_MOVE_BATCH_QUERY_LIMIT = 10_000;
|
|
64
|
+
const DEFAULT_MIN_BUCKET_CHANGES = 10;
|
|
64
65
|
|
|
65
66
|
/** This default is primarily for tests. */
|
|
66
67
|
const DEFAULT_MEMORY_LIMIT_MB = 64;
|
|
@@ -73,6 +74,7 @@ export class MongoCompactor {
|
|
|
73
74
|
private moveBatchLimit: number;
|
|
74
75
|
private moveBatchQueryLimit: number;
|
|
75
76
|
private clearBatchLimit: number;
|
|
77
|
+
private minBucketChanges: number;
|
|
76
78
|
private maxOpId: bigint;
|
|
77
79
|
private buckets: string[] | undefined;
|
|
78
80
|
private signal?: AbortSignal;
|
|
@@ -88,6 +90,7 @@ export class MongoCompactor {
|
|
|
88
90
|
this.moveBatchLimit = options?.moveBatchLimit ?? DEFAULT_MOVE_BATCH_LIMIT;
|
|
89
91
|
this.moveBatchQueryLimit = options?.moveBatchQueryLimit ?? DEFAULT_MOVE_BATCH_QUERY_LIMIT;
|
|
90
92
|
this.clearBatchLimit = options?.clearBatchLimit ?? DEFAULT_CLEAR_BATCH_LIMIT;
|
|
93
|
+
this.minBucketChanges = options?.minBucketChanges ?? DEFAULT_MIN_BUCKET_CHANGES;
|
|
91
94
|
this.maxOpId = options?.maxOpId ?? 0n;
|
|
92
95
|
this.buckets = options?.compactBuckets;
|
|
93
96
|
this.signal = options?.signal;
|
|
@@ -113,14 +116,26 @@ export class MongoCompactor {
|
|
|
113
116
|
|
|
114
117
|
private async compactDirtyBuckets() {
|
|
115
118
|
while (!this.signal?.aborted) {
|
|
116
|
-
// Process all buckets with
|
|
117
|
-
|
|
119
|
+
// Process all buckets with 10 or more changes since last time.
|
|
120
|
+
// We exclude the last 100 compacted buckets, to avoid repeatedly re-compacting the same buckets over and over
|
|
121
|
+
// if they are modified while compacting.
|
|
122
|
+
const TRACK_RECENTLY_COMPACTED_NUMBER = 100;
|
|
123
|
+
|
|
124
|
+
let recentlyCompacted: string[] = [];
|
|
125
|
+
const buckets = await this.dirtyBucketBatch({
|
|
126
|
+
minBucketChanges: this.minBucketChanges,
|
|
127
|
+
exclude: recentlyCompacted
|
|
128
|
+
});
|
|
118
129
|
if (buckets.length == 0) {
|
|
119
130
|
// All done
|
|
120
131
|
break;
|
|
121
132
|
}
|
|
122
|
-
for (let bucket of buckets) {
|
|
133
|
+
for (let { bucket } of buckets) {
|
|
123
134
|
await this.compactSingleBucket(bucket);
|
|
135
|
+
recentlyCompacted.push(bucket);
|
|
136
|
+
}
|
|
137
|
+
if (recentlyCompacted.length > TRACK_RECENTLY_COMPACTED_NUMBER) {
|
|
138
|
+
recentlyCompacted = recentlyCompacted.slice(-TRACK_RECENTLY_COMPACTED_NUMBER);
|
|
124
139
|
}
|
|
125
140
|
}
|
|
126
141
|
}
|
|
@@ -482,10 +497,20 @@ export class MongoCompactor {
|
|
|
482
497
|
break;
|
|
483
498
|
}
|
|
484
499
|
const start = Date.now();
|
|
485
|
-
logger.info(`Calculating checksums for batch of ${buckets.length} buckets
|
|
500
|
+
logger.info(`Calculating checksums for batch of ${buckets.length} buckets`);
|
|
486
501
|
|
|
487
|
-
|
|
488
|
-
|
|
502
|
+
// Filter batch by estimated bucket size, to reduce possibility of timeouts
|
|
503
|
+
let checkBuckets: typeof buckets = [];
|
|
504
|
+
let totalCountEstimate = 0;
|
|
505
|
+
for (let bucket of buckets) {
|
|
506
|
+
checkBuckets.push(bucket);
|
|
507
|
+
totalCountEstimate += bucket.estimatedCount;
|
|
508
|
+
if (totalCountEstimate > 50_000) {
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
await this.updateChecksumsBatch(checkBuckets.map((b) => b.bucket));
|
|
513
|
+
logger.info(`Updated checksums for batch of ${checkBuckets.length} buckets in ${Date.now() - start}ms`);
|
|
489
514
|
count += buckets.length;
|
|
490
515
|
}
|
|
491
516
|
return { buckets: count };
|
|
@@ -497,7 +522,10 @@ export class MongoCompactor {
|
|
|
497
522
|
* This cannot be used to iterate on its own - the client is expected to process these buckets and
|
|
498
523
|
* set estimate_since_compact.count: 0 when done, before fetching the next batch.
|
|
499
524
|
*/
|
|
500
|
-
private async dirtyBucketBatch(options: {
|
|
525
|
+
private async dirtyBucketBatch(options: {
|
|
526
|
+
minBucketChanges: number;
|
|
527
|
+
exclude?: string[];
|
|
528
|
+
}): Promise<{ bucket: string; estimatedCount: number }[]> {
|
|
501
529
|
if (options.minBucketChanges <= 0) {
|
|
502
530
|
throw new ReplicationAssertionError('minBucketChanges must be >= 1');
|
|
503
531
|
}
|
|
@@ -506,22 +534,28 @@ export class MongoCompactor {
|
|
|
506
534
|
.find(
|
|
507
535
|
{
|
|
508
536
|
'_id.g': this.group_id,
|
|
509
|
-
'estimate_since_compact.count': { $gte: options.minBucketChanges }
|
|
537
|
+
'estimate_since_compact.count': { $gte: options.minBucketChanges },
|
|
538
|
+
'_id.b': { $nin: options.exclude ?? [] }
|
|
510
539
|
},
|
|
511
540
|
{
|
|
512
541
|
projection: {
|
|
513
|
-
_id: 1
|
|
542
|
+
_id: 1,
|
|
543
|
+
estimate_since_compact: 1,
|
|
544
|
+
compacted_state: 1
|
|
514
545
|
},
|
|
515
546
|
sort: {
|
|
516
547
|
'estimate_since_compact.count': -1
|
|
517
548
|
},
|
|
518
|
-
limit:
|
|
549
|
+
limit: 200,
|
|
519
550
|
maxTimeMS: MONGO_OPERATION_TIMEOUT_MS
|
|
520
551
|
}
|
|
521
552
|
)
|
|
522
553
|
.toArray();
|
|
523
554
|
|
|
524
|
-
return dirtyBuckets.map((bucket) =>
|
|
555
|
+
return dirtyBuckets.map((bucket) => ({
|
|
556
|
+
bucket: bucket._id.b,
|
|
557
|
+
estimatedCount: bucket.estimate_since_compact!.count + (bucket.compacted_state?.count ?? 0)
|
|
558
|
+
}));
|
|
525
559
|
}
|
|
526
560
|
|
|
527
561
|
private async updateChecksumsBatch(buckets: string[]) {
|
|
@@ -33,10 +33,19 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
|
|
|
33
33
|
);
|
|
34
34
|
|
|
35
35
|
if (doc == null) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
// Query the existing lock to get the expiration time (best effort - it may have been released in the meantime).
|
|
37
|
+
const heldLock = await db.sync_rules.findOne({ _id: sync_rules.id }, { projection: { lock: 1 } });
|
|
38
|
+
if (heldLock?.lock?.expires_at) {
|
|
39
|
+
throw new ServiceError(
|
|
40
|
+
ErrorCode.PSYNC_S1003,
|
|
41
|
+
`Sync rules: ${sync_rules.id} have been locked by another process for replication, expiring at ${heldLock.lock.expires_at.toISOString()}.`
|
|
42
|
+
);
|
|
43
|
+
} else {
|
|
44
|
+
throw new ServiceError(
|
|
45
|
+
ErrorCode.PSYNC_S1003,
|
|
46
|
+
`Sync rules: ${sync_rules.id} have been locked by another process for replication.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
40
49
|
}
|
|
41
50
|
return new MongoSyncRulesLock(db, sync_rules.id, lockId);
|
|
42
51
|
}
|
package/src/utils/util.ts
CHANGED
|
@@ -125,24 +125,15 @@ export const createPaginatedConnectionQuery = async <T extends mongo.Document>(
|
|
|
125
125
|
if (!cursor) {
|
|
126
126
|
return query;
|
|
127
127
|
}
|
|
128
|
+
const connected_at = query.connected_at
|
|
129
|
+
? { $lt: new Date(cursor), $gte: query.connected_at.$gte }
|
|
130
|
+
: { $lt: new Date(cursor) };
|
|
128
131
|
return {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
{
|
|
132
|
-
/** We are using the connected at date as the cursor so that the functionality works the same on Postgres implementation
|
|
133
|
-
* The id field in postgres is an uuid, this will work similarly to the ObjectId in Mongodb
|
|
134
|
-
* */
|
|
135
|
-
connected_at: {
|
|
136
|
-
$lt: new Date(cursor)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
]
|
|
132
|
+
...query,
|
|
133
|
+
connected_at
|
|
140
134
|
} as mongo.Filter<T>;
|
|
141
135
|
};
|
|
142
136
|
|
|
143
|
-
/** cursor.count() deprecated */
|
|
144
|
-
const total = await collection.countDocuments(query);
|
|
145
|
-
|
|
146
137
|
const findCursor = collection.find(createQuery(cursor), {
|
|
147
138
|
sort: {
|
|
148
139
|
/** We are sorting by connected at date descending to match cursor Postgres implementation */
|
|
@@ -152,12 +143,14 @@ export const createPaginatedConnectionQuery = async <T extends mongo.Document>(
|
|
|
152
143
|
|
|
153
144
|
const items = await findCursor.limit(limit).toArray();
|
|
154
145
|
const count = items.length;
|
|
146
|
+
/** The returned total has been defaulted to 0 due to the overhead using documentCount from the mogo driver.
|
|
147
|
+
* cursor.count has been deprecated.
|
|
148
|
+
* */
|
|
155
149
|
return {
|
|
156
150
|
items,
|
|
157
|
-
total,
|
|
158
151
|
count,
|
|
159
152
|
/** Setting the cursor to the connected at date of the last item in the list */
|
|
160
153
|
cursor: count === limit ? items[items.length - 1].connected_at.toISOString() : undefined,
|
|
161
|
-
more: count
|
|
154
|
+
more: !(count !== limit)
|
|
162
155
|
};
|
|
163
156
|
};
|
|
@@ -227,7 +227,6 @@ exports[`Report storage tests > Should show paginated response of all connection
|
|
|
227
227
|
},
|
|
228
228
|
],
|
|
229
229
|
"more": false,
|
|
230
|
-
"total": 1,
|
|
231
230
|
}
|
|
232
231
|
`;
|
|
233
232
|
|
|
@@ -262,7 +261,6 @@ exports[`Report storage tests > Should show paginated response of all connection
|
|
|
262
261
|
},
|
|
263
262
|
],
|
|
264
263
|
"more": true,
|
|
265
|
-
"total": 8,
|
|
266
264
|
}
|
|
267
265
|
`;
|
|
268
266
|
|
|
@@ -271,18 +269,74 @@ exports[`Report storage tests > Should show paginated response of all connection
|
|
|
271
269
|
"count": 4,
|
|
272
270
|
"cursor": undefined,
|
|
273
271
|
"items": [
|
|
272
|
+
{
|
|
273
|
+
"client_id": "client_three",
|
|
274
|
+
"sdk": "powersync-js/1.21.2",
|
|
275
|
+
"user_agent": "powersync-js/1.21.0 powersync-web Firefox/141 linux",
|
|
276
|
+
"user_id": "user_three",
|
|
277
|
+
},
|
|
274
278
|
{
|
|
275
279
|
"client_id": "client_one",
|
|
276
|
-
"sdk": "powersync-
|
|
277
|
-
"user_agent": "powersync-
|
|
280
|
+
"sdk": "powersync-js/1.24.5",
|
|
281
|
+
"user_agent": "powersync-js/1.21.0 powersync-web Firefox/141 linux",
|
|
282
|
+
"user_id": "user_week",
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"client_id": "client_month",
|
|
286
|
+
"sdk": "powersync-js/1.23.6",
|
|
287
|
+
"user_agent": "powersync-js/1.23.0 powersync-web Firefox/141 linux",
|
|
288
|
+
"user_id": "user_month",
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"client_id": "client_expired",
|
|
292
|
+
"sdk": "powersync-js/1.23.7",
|
|
293
|
+
"user_agent": "powersync-js/1.23.0 powersync-web Firefox/141 linux",
|
|
294
|
+
"user_id": "user_expired",
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
"more": false,
|
|
298
|
+
}
|
|
299
|
+
`;
|
|
300
|
+
|
|
301
|
+
exports[`Report storage tests > Should show paginated response of all connections with a limit with date range 1`] = `
|
|
302
|
+
{
|
|
303
|
+
"count": 4,
|
|
304
|
+
"cursor": "<removed-for-snapshot>",
|
|
305
|
+
"items": [
|
|
306
|
+
{
|
|
307
|
+
"client_id": "",
|
|
308
|
+
"sdk": "unknown",
|
|
309
|
+
"user_agent": "Dart (flutter-web) Chrome/128 android",
|
|
278
310
|
"user_id": "user_one",
|
|
279
311
|
},
|
|
280
312
|
{
|
|
281
|
-
"client_id": "
|
|
282
|
-
"sdk": "powersync-js/1.21.
|
|
313
|
+
"client_id": "client_two",
|
|
314
|
+
"sdk": "powersync-js/1.21.1",
|
|
315
|
+
"user_agent": "powersync-js/1.21.0 powersync-web Chromium/138 linux",
|
|
316
|
+
"user_id": "user_two",
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
"client_id": "client_three",
|
|
320
|
+
"sdk": "powersync-js/1.21.2",
|
|
283
321
|
"user_agent": "powersync-js/1.21.0 powersync-web Firefox/141 linux",
|
|
284
|
-
"user_id": "
|
|
322
|
+
"user_id": "user_three",
|
|
285
323
|
},
|
|
324
|
+
{
|
|
325
|
+
"client_id": "client_one",
|
|
326
|
+
"sdk": "powersync-js/1.24.5",
|
|
327
|
+
"user_agent": "powersync-js/1.21.0 powersync-web Firefox/141 linux",
|
|
328
|
+
"user_id": "user_week",
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
"more": true,
|
|
332
|
+
}
|
|
333
|
+
`;
|
|
334
|
+
|
|
335
|
+
exports[`Report storage tests > Should show paginated response of all connections with a limit with date range 2`] = `
|
|
336
|
+
{
|
|
337
|
+
"count": 2,
|
|
338
|
+
"cursor": undefined,
|
|
339
|
+
"items": [
|
|
286
340
|
{
|
|
287
341
|
"client_id": "",
|
|
288
342
|
"sdk": "unknown",
|
|
@@ -295,9 +349,20 @@ exports[`Report storage tests > Should show paginated response of all connection
|
|
|
295
349
|
"user_agent": "powersync-js/1.21.0 powersync-web Chromium/138 linux",
|
|
296
350
|
"user_id": "user_two",
|
|
297
351
|
},
|
|
352
|
+
{
|
|
353
|
+
"client_id": "client_three",
|
|
354
|
+
"sdk": "powersync-js/1.21.2",
|
|
355
|
+
"user_agent": "powersync-js/1.21.0 powersync-web Firefox/141 linux",
|
|
356
|
+
"user_id": "user_three",
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
"client_id": "client_one",
|
|
360
|
+
"sdk": "powersync-js/1.24.5",
|
|
361
|
+
"user_agent": "powersync-js/1.21.0 powersync-web Firefox/141 linux",
|
|
362
|
+
"user_id": "user_week",
|
|
363
|
+
},
|
|
298
364
|
],
|
|
299
|
-
"more":
|
|
300
|
-
"total": 8,
|
|
365
|
+
"more": false,
|
|
301
366
|
}
|
|
302
367
|
`;
|
|
303
368
|
|
|
@@ -320,7 +385,6 @@ exports[`Report storage tests > Should show paginated response of connections of
|
|
|
320
385
|
},
|
|
321
386
|
],
|
|
322
387
|
"more": false,
|
|
323
|
-
"total": 2,
|
|
324
388
|
}
|
|
325
389
|
`;
|
|
326
390
|
|
|
@@ -367,6 +431,21 @@ exports[`Report storage tests > Should show paginated response of connections ov
|
|
|
367
431
|
},
|
|
368
432
|
],
|
|
369
433
|
"more": false,
|
|
370
|
-
|
|
434
|
+
}
|
|
435
|
+
`;
|
|
436
|
+
|
|
437
|
+
exports[`Report storage tests > Should show paginated response of connections over a date range of specified client_id and user_id 1`] = `
|
|
438
|
+
{
|
|
439
|
+
"count": 1,
|
|
440
|
+
"cursor": undefined,
|
|
441
|
+
"items": [
|
|
442
|
+
{
|
|
443
|
+
"client_id": "client_one",
|
|
444
|
+
"sdk": "powersync-dart/1.6.4",
|
|
445
|
+
"user_agent": "powersync-dart/1.6.4 Dart (flutter-web) Chrome/128 android",
|
|
446
|
+
"user_id": "user_one",
|
|
447
|
+
},
|
|
448
|
+
],
|
|
449
|
+
"more": false,
|
|
371
450
|
}
|
|
372
451
|
`;
|