@powersync/service-module-mongodb-storage 0.18.0 → 0.18.2
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 +28 -0
- package/dist/storage/MongoBucketStorage.d.ts +3 -0
- package/dist/storage/MongoBucketStorage.js +5 -3
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +2 -2
- package/dist/storage/implementation/MongoBucketBatch.js +19 -10
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoChecksums.d.ts +29 -4
- package/dist/storage/implementation/MongoChecksums.js +32 -10
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +1 -1
- package/dist/storage/implementation/MongoCompactor.js +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.js +1 -1
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +4 -2
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +9 -0
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +7 -4
- package/dist/storage/implementation/MongoSyncBucketStorage.js +11 -4
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +47 -12
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/db.js +8 -1
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +17 -2
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +4 -4
- package/dist/storage/implementation/v1/MongoChecksumsV1.js +7 -6
- package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.js +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +4 -3
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +17 -4
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +20 -2
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +3 -3
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +8 -7
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +4 -3
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +16 -3
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/types/types.d.ts +12 -1
- package/dist/types/types.js +14 -1
- package/dist/types/types.js.map +1 -1
- package/package.json +7 -7
- package/src/storage/MongoBucketStorage.ts +7 -3
- package/src/storage/implementation/MongoBucketBatch.ts +23 -13
- package/src/storage/implementation/MongoChecksums.ts +84 -15
- package/src/storage/implementation/MongoCompactor.ts +1 -1
- package/src/storage/implementation/MongoParameterCompactor.ts +1 -1
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +4 -2
- package/src/storage/implementation/MongoStorageProvider.ts +10 -0
- package/src/storage/implementation/MongoSyncBucketStorage.ts +18 -8
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +62 -15
- package/src/storage/implementation/db.ts +8 -1
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +18 -2
- package/src/storage/implementation/v1/MongoChecksumsV1.ts +20 -11
- package/src/storage/implementation/v1/MongoCompactorV1.ts +2 -1
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +23 -6
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +20 -2
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +23 -11
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +22 -5
- package/src/types/types.ts +17 -1
- package/test/src/storage_compacting.test.ts +4 -4
- package/test/src/storage_sync.test.ts +51 -5
- package/test/src/storeCurrentData.test.ts +9 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -203,6 +203,8 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
203
203
|
|
|
204
204
|
await this.flush(options);
|
|
205
205
|
|
|
206
|
+
using _ = this.tracer.span('storage', 'commit');
|
|
207
|
+
|
|
206
208
|
const now = new Date();
|
|
207
209
|
|
|
208
210
|
await this.db.write_checkpoints.updateMany(
|
|
@@ -358,6 +360,7 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
358
360
|
}
|
|
359
361
|
|
|
360
362
|
async setResumeLsn(lsn: string): Promise<void> {
|
|
363
|
+
using _ = this.tracer.span('storage', 'set_resume_lsn');
|
|
361
364
|
await this.db.sync_rules.updateOne(
|
|
362
365
|
{
|
|
363
366
|
_id: this.replicationStreamId
|
|
@@ -367,7 +370,13 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
367
370
|
snapshot_lsn: lsn
|
|
368
371
|
}
|
|
369
372
|
},
|
|
370
|
-
{
|
|
373
|
+
{
|
|
374
|
+
session: this.session,
|
|
375
|
+
// Losing occasional resume LSN is fine. That may mean reprocessing
|
|
376
|
+
// some source changes in some edge cases, which is not an issue since
|
|
377
|
+
// changes are processed in an idempotent way.
|
|
378
|
+
writeConcern: { w: 1 }
|
|
379
|
+
}
|
|
371
380
|
);
|
|
372
381
|
}
|
|
373
382
|
|
|
@@ -486,7 +495,12 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
486
495
|
|
|
487
496
|
const session = this.session;
|
|
488
497
|
let activated = false;
|
|
498
|
+
let needsFutureActivationCheck = true;
|
|
489
499
|
await session.withTransaction(async () => {
|
|
500
|
+
// Reset on transaction retries.
|
|
501
|
+
activated = false;
|
|
502
|
+
needsFutureActivationCheck = true;
|
|
503
|
+
|
|
490
504
|
const doc = (await this.db.sync_rules.findOne(
|
|
491
505
|
{ _id: this.replicationStreamId },
|
|
492
506
|
{ session }
|
|
@@ -518,13 +532,15 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
518
532
|
);
|
|
519
533
|
activated = true;
|
|
520
534
|
} else if (doc?.state != storage.SyncRuleState.PROCESSING) {
|
|
521
|
-
|
|
535
|
+
needsFutureActivationCheck = false;
|
|
522
536
|
}
|
|
523
537
|
});
|
|
524
538
|
if (activated) {
|
|
525
539
|
this.logger.info(`Activated new replication stream at ${lsn}`);
|
|
526
540
|
await this.db.notifyCheckpoint();
|
|
527
541
|
this.needsActivation = false;
|
|
542
|
+
} else if (!needsFutureActivationCheck) {
|
|
543
|
+
this.needsActivation = false;
|
|
528
544
|
}
|
|
529
545
|
}
|
|
530
546
|
}
|
|
@@ -13,15 +13,16 @@ import {
|
|
|
13
13
|
checksumFromAggregate,
|
|
14
14
|
DEFAULT_OPERATION_BATCH_LIMIT,
|
|
15
15
|
FetchPartialBucketChecksumByBucket,
|
|
16
|
-
MongoChecksums
|
|
16
|
+
MongoChecksums,
|
|
17
|
+
MongoChecksumSessionContext
|
|
17
18
|
} from '../MongoChecksums.js';
|
|
18
19
|
import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
|
|
19
|
-
|
|
20
20
|
export class MongoChecksumsV1 extends MongoChecksums {
|
|
21
21
|
declare protected readonly db: VersionedPowerSyncMongoV1;
|
|
22
22
|
|
|
23
23
|
async computePartialChecksumsDirectByBucket(
|
|
24
|
-
batch: FetchPartialBucketChecksumByBucket[]
|
|
24
|
+
batch: FetchPartialBucketChecksumByBucket[],
|
|
25
|
+
context: MongoChecksumSessionContext
|
|
25
26
|
): Promise<PartialChecksumMap> {
|
|
26
27
|
const collection = this.db.bucketDataV1;
|
|
27
28
|
const createFilter = (request: FetchPartialBucketChecksumByBucket) => ({
|
|
@@ -95,8 +96,7 @@ export class MongoChecksumsV1 extends MongoChecksums {
|
|
|
95
96
|
{ $sort: { _id: 1 } }
|
|
96
97
|
],
|
|
97
98
|
{
|
|
98
|
-
|
|
99
|
-
readConcern: 'snapshot',
|
|
99
|
+
...context.readOptions,
|
|
100
100
|
maxTimeMS: lib_mongo.MONGO_CHECKSUM_TIMEOUT_MS
|
|
101
101
|
}
|
|
102
102
|
)
|
|
@@ -164,7 +164,8 @@ export class MongoChecksumsV1 extends MongoChecksums {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
protected async fetchPreStates(
|
|
167
|
-
batch: FetchPartialBucketChecksum[]
|
|
167
|
+
batch: FetchPartialBucketChecksum[],
|
|
168
|
+
context: MongoChecksumSessionContext
|
|
168
169
|
): Promise<Map<string, { opId: InternalOpId; checksum: BucketChecksum }>> {
|
|
169
170
|
const preFilters = batch
|
|
170
171
|
.filter((request) => request.start == null)
|
|
@@ -182,9 +183,14 @@ export class MongoChecksumsV1 extends MongoChecksums {
|
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
const states = await this.db.bucketStateV1
|
|
185
|
-
.find(
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
.find(
|
|
187
|
+
{
|
|
188
|
+
$or: preFilters
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
...context.readOptions
|
|
192
|
+
}
|
|
193
|
+
)
|
|
188
194
|
.toArray();
|
|
189
195
|
|
|
190
196
|
for (const state of states) {
|
|
@@ -202,7 +208,10 @@ export class MongoChecksumsV1 extends MongoChecksums {
|
|
|
202
208
|
return preStates;
|
|
203
209
|
}
|
|
204
210
|
|
|
205
|
-
protected async computePartialChecksumsInternal(
|
|
206
|
-
|
|
211
|
+
protected async computePartialChecksumsInternal(
|
|
212
|
+
batch: FetchPartialBucketChecksum[],
|
|
213
|
+
context: MongoChecksumSessionContext
|
|
214
|
+
): Promise<PartialChecksumMap> {
|
|
215
|
+
return this.computePartialChecksumsDirectByBucket(batch, context);
|
|
207
216
|
}
|
|
208
217
|
}
|
|
@@ -41,6 +41,7 @@ import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
|
|
|
41
41
|
export interface MongoSyncBucketStorageContextV1 {
|
|
42
42
|
db: VersionedPowerSyncMongoV1;
|
|
43
43
|
replicationStreamId: number;
|
|
44
|
+
readPreference?: mongo.ReadPreference;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
|
|
@@ -172,6 +173,7 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
|
|
|
172
173
|
protected createMongoChecksums(options: MongoSyncBucketStorageOptions): MongoChecksums {
|
|
173
174
|
return new MongoChecksumsV1(this.db, this.replicationStreamId, {
|
|
174
175
|
...options.checksumOptions,
|
|
176
|
+
checksumCacheTtlMs: options.checksumCacheTtlMs,
|
|
175
177
|
storageConfig: options?.storageConfig
|
|
176
178
|
});
|
|
177
179
|
}
|
|
@@ -190,7 +192,8 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
|
|
|
190
192
|
protected get versionContext(): MongoSyncBucketStorageContextV1 {
|
|
191
193
|
return {
|
|
192
194
|
db: this.db,
|
|
193
|
-
replicationStreamId: this.replicationStreamId
|
|
195
|
+
replicationStreamId: this.replicationStreamId,
|
|
196
|
+
readPreference: this.readPreference
|
|
194
197
|
};
|
|
195
198
|
}
|
|
196
199
|
|
|
@@ -203,7 +206,7 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
|
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
protected getBucketDataBatchImpl(
|
|
206
|
-
checkpoint:
|
|
209
|
+
checkpoint: MongoSyncBucketStorageCheckpoint,
|
|
207
210
|
dataBuckets: storage.BucketDataRequest[],
|
|
208
211
|
options?: storage.BucketDataBatchOptions
|
|
209
212
|
): AsyncIterable<storage.SyncBucketDataChunk> {
|
|
@@ -369,20 +372,32 @@ export async function getParameterSetsV1(
|
|
|
369
372
|
|
|
370
373
|
export async function* getBucketDataBatchV1(
|
|
371
374
|
ctx: MongoSyncBucketStorageContextV1,
|
|
372
|
-
checkpoint:
|
|
375
|
+
checkpoint: MongoSyncBucketStorageCheckpoint,
|
|
373
376
|
dataBuckets: storage.BucketDataRequest[],
|
|
374
377
|
options?: storage.BucketDataBatchOptions
|
|
375
378
|
): AsyncIterable<storage.SyncBucketDataChunk> {
|
|
376
379
|
if (dataBuckets.length == 0) {
|
|
377
380
|
return;
|
|
378
381
|
}
|
|
382
|
+
const readPreference = options?.requestHint == 'bulk' ? ctx.readPreference : undefined;
|
|
383
|
+
const readConcern = ctx.readPreference == null ? undefined : 'majority';
|
|
384
|
+
const session =
|
|
385
|
+
readPreference == null || checkpoint.snapshotTime == null
|
|
386
|
+
? undefined
|
|
387
|
+
: ctx.db.client.startSession({ causalConsistency: true });
|
|
388
|
+
await using _ = { [Symbol.asyncDispose]: async () => session?.endSession() };
|
|
389
|
+
|
|
390
|
+
if (session != null) {
|
|
391
|
+
session.advanceOperationTime(checkpoint.snapshotTime);
|
|
392
|
+
}
|
|
393
|
+
|
|
379
394
|
let filters: mongo.Filter<BucketDataDocumentV1>[] = [];
|
|
380
395
|
const bucketMap = new Map(dataBuckets.map((request) => [request.bucket, request.start]));
|
|
381
396
|
|
|
382
|
-
if (checkpoint == null) {
|
|
397
|
+
if (checkpoint.checkpoint == null) {
|
|
383
398
|
throw new Error('checkpoint is null');
|
|
384
399
|
}
|
|
385
|
-
const end = checkpoint;
|
|
400
|
+
const end = checkpoint.checkpoint;
|
|
386
401
|
for (let { bucket: name, start } of dataBuckets) {
|
|
387
402
|
filters.push({
|
|
388
403
|
_id: {
|
|
@@ -410,7 +425,9 @@ export async function* getBucketDataBatchV1(
|
|
|
410
425
|
$or: filters
|
|
411
426
|
},
|
|
412
427
|
{
|
|
413
|
-
session
|
|
428
|
+
session,
|
|
429
|
+
readPreference,
|
|
430
|
+
readConcern,
|
|
414
431
|
sort: { _id: 1 },
|
|
415
432
|
limit: batchLimit,
|
|
416
433
|
batchSize: batchLimit + 1,
|
|
@@ -243,6 +243,8 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
243
243
|
|
|
244
244
|
await this.flush(options);
|
|
245
245
|
|
|
246
|
+
using _ = this.tracer.span('storage', 'commit');
|
|
247
|
+
|
|
246
248
|
const now = new Date();
|
|
247
249
|
|
|
248
250
|
await this.db.write_checkpoints.updateMany(
|
|
@@ -418,6 +420,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
418
420
|
}
|
|
419
421
|
|
|
420
422
|
async setResumeLsn(lsn: string): Promise<void> {
|
|
423
|
+
using _ = this.tracer.span('storage', 'set_resume_lsn');
|
|
421
424
|
await this.db.sync_rules.updateOne(
|
|
422
425
|
{
|
|
423
426
|
_id: this.replicationStreamId
|
|
@@ -427,7 +430,13 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
427
430
|
resume_lsn: lsn
|
|
428
431
|
}
|
|
429
432
|
},
|
|
430
|
-
{
|
|
433
|
+
{
|
|
434
|
+
session: this.session,
|
|
435
|
+
// Losing occasional resume LSN is fine. That may mean reprocessing
|
|
436
|
+
// some source changes in some edge cases, which is not an issue since
|
|
437
|
+
// changes are processed in an idempotent way.
|
|
438
|
+
writeConcern: { w: 1 }
|
|
439
|
+
}
|
|
431
440
|
);
|
|
432
441
|
}
|
|
433
442
|
|
|
@@ -438,7 +447,12 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
438
447
|
|
|
439
448
|
const session = this.session;
|
|
440
449
|
let activated = false;
|
|
450
|
+
let needsFutureActivationCheck = true;
|
|
441
451
|
await session.withTransaction(async () => {
|
|
452
|
+
// Reset on transaction retries.
|
|
453
|
+
needsFutureActivationCheck = true;
|
|
454
|
+
activated = false;
|
|
455
|
+
|
|
442
456
|
const doc = await this.db.sync_rules.findOne(
|
|
443
457
|
{
|
|
444
458
|
_id: this.replicationStreamId,
|
|
@@ -518,13 +532,17 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
518
532
|
);
|
|
519
533
|
activated = true;
|
|
520
534
|
} else if (doc.state != storage.SyncRuleState.PROCESSING && doc.state != storage.SyncRuleState.ACTIVE) {
|
|
521
|
-
|
|
535
|
+
needsFutureActivationCheck = false;
|
|
536
|
+
} else if (doc.state == storage.SyncRuleState.ACTIVE && processingStates.length == 0) {
|
|
537
|
+
needsFutureActivationCheck = false;
|
|
522
538
|
}
|
|
523
539
|
});
|
|
524
540
|
if (activated) {
|
|
525
541
|
this.logger.info(`Activated new replication stream at ${lsn}`);
|
|
526
542
|
await this.db.notifyCheckpoint();
|
|
527
543
|
this.needsActivationV3 = false;
|
|
544
|
+
} else if (!needsFutureActivationCheck) {
|
|
545
|
+
this.needsActivationV3 = false;
|
|
528
546
|
}
|
|
529
547
|
}
|
|
530
548
|
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
FetchPartialBucketChecksumByBucket,
|
|
16
16
|
FetchPartialBucketChecksumByDefinition,
|
|
17
17
|
MongoChecksumOptions,
|
|
18
|
-
MongoChecksums
|
|
18
|
+
MongoChecksums,
|
|
19
|
+
MongoChecksumSessionContext
|
|
19
20
|
} from '../MongoChecksums.js';
|
|
20
21
|
import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
21
22
|
import { BucketDataDocumentV3 } from './models.js';
|
|
@@ -66,7 +67,8 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
|
|
|
66
67
|
for (const [definitionId, requests] of requestsByDefinition.entries()) {
|
|
67
68
|
const groupResults = await this.computeChecksumsByDefinition(
|
|
68
69
|
requests,
|
|
69
|
-
this.db.bucketData(this.group_id, definitionId)
|
|
70
|
+
this.db.bucketData(this.group_id, definitionId),
|
|
71
|
+
{ readOptions: { readConcern: 'snapshot' } }
|
|
70
72
|
);
|
|
71
73
|
for (const checksum of groupResults.values()) {
|
|
72
74
|
results.set(checksum.bucket, checksum);
|
|
@@ -79,7 +81,8 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
protected async fetchPreStates(
|
|
82
|
-
batch: FetchPartialBucketChecksum[]
|
|
84
|
+
batch: FetchPartialBucketChecksum[],
|
|
85
|
+
context: MongoChecksumSessionContext
|
|
83
86
|
): Promise<Map<string, { opId: InternalOpId; checksum: BucketChecksum }>> {
|
|
84
87
|
const normalizedBatch = batch.map((request) => ({
|
|
85
88
|
bucket: request.bucket,
|
|
@@ -105,9 +108,14 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
|
|
|
105
108
|
|
|
106
109
|
const states = await this.db
|
|
107
110
|
.bucketState(this.group_id)
|
|
108
|
-
.find(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
.find(
|
|
112
|
+
{
|
|
113
|
+
$or: preFilters
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
...context.readOptions
|
|
117
|
+
}
|
|
118
|
+
)
|
|
111
119
|
.toArray();
|
|
112
120
|
|
|
113
121
|
for (const state of states) {
|
|
@@ -125,7 +133,10 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
|
|
|
125
133
|
return preStates;
|
|
126
134
|
}
|
|
127
135
|
|
|
128
|
-
protected async computePartialChecksumsInternal(
|
|
136
|
+
protected async computePartialChecksumsInternal(
|
|
137
|
+
batch: FetchPartialBucketChecksum[],
|
|
138
|
+
context: MongoChecksumSessionContext
|
|
139
|
+
): Promise<PartialChecksumMap> {
|
|
129
140
|
const normalized = batch.map((request) => ({
|
|
130
141
|
bucket: request.bucket,
|
|
131
142
|
definitionId: this.syncConfigMapping().bucketSourceId(request.source),
|
|
@@ -145,7 +156,8 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
|
|
|
145
156
|
for (const [definitionId, requests] of requestsByDefinition.entries()) {
|
|
146
157
|
const groupResults = await this.computeChecksumsByDefinition(
|
|
147
158
|
requests,
|
|
148
|
-
this.db.bucketData(this.group_id, definitionId)
|
|
159
|
+
this.db.bucketData(this.group_id, definitionId),
|
|
160
|
+
context
|
|
149
161
|
);
|
|
150
162
|
for (const checksum of groupResults.values()) {
|
|
151
163
|
results.set(checksum.bucket, checksum);
|
|
@@ -159,7 +171,8 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
|
|
|
159
171
|
|
|
160
172
|
private async computeChecksumsByDefinition(
|
|
161
173
|
batch: FetchPartialBucketChecksumByBucket[],
|
|
162
|
-
collection: lib_mongo.mongo.Collection<BucketDataDocumentV3
|
|
174
|
+
collection: lib_mongo.mongo.Collection<BucketDataDocumentV3>,
|
|
175
|
+
context: MongoChecksumSessionContext
|
|
163
176
|
): Promise<PartialChecksumMap> {
|
|
164
177
|
const requests = new Map<string, FetchPartialBucketChecksumByBucket>();
|
|
165
178
|
for (let request of batch) {
|
|
@@ -169,8 +182,7 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
|
|
|
169
182
|
const pipeline = this.buildPartialChecksumPipeline(requests);
|
|
170
183
|
const aggregate = await collection
|
|
171
184
|
.aggregate(pipeline, {
|
|
172
|
-
|
|
173
|
-
readConcern: 'snapshot',
|
|
185
|
+
...context.readOptions,
|
|
174
186
|
maxTimeMS: lib_mongo.MONGO_CHECKSUM_TIMEOUT_MS
|
|
175
187
|
})
|
|
176
188
|
.toArray()
|
|
@@ -42,6 +42,7 @@ import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
|
42
42
|
export interface MongoSyncBucketStorageContextV3 {
|
|
43
43
|
db: VersionedPowerSyncMongoV3;
|
|
44
44
|
replicationStreamId: number;
|
|
45
|
+
readPreference?: mongo.ReadPreference;
|
|
45
46
|
/**
|
|
46
47
|
* Persisted mapping of the single sync config that read operations are served from.
|
|
47
48
|
*
|
|
@@ -171,6 +172,7 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
|
|
|
171
172
|
protected createMongoChecksums(options: MongoSyncBucketStorageOptions): MongoChecksums {
|
|
172
173
|
return new MongoChecksumsV3(this.db, this.replicationStreamId, {
|
|
173
174
|
...options.checksumOptions,
|
|
175
|
+
checksumCacheTtlMs: options.checksumCacheTtlMs,
|
|
174
176
|
storageConfig: options?.storageConfig,
|
|
175
177
|
syncConfigMapping: () => this.singleSyncConfigMapping()
|
|
176
178
|
});
|
|
@@ -336,6 +338,7 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
|
|
|
336
338
|
return {
|
|
337
339
|
db: this.db,
|
|
338
340
|
replicationStreamId: this.replicationStreamId,
|
|
341
|
+
readPreference: this.readPreference,
|
|
339
342
|
get mapping() {
|
|
340
343
|
return self.singleSyncConfigMapping();
|
|
341
344
|
}
|
|
@@ -351,7 +354,7 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
|
|
|
351
354
|
}
|
|
352
355
|
|
|
353
356
|
protected getBucketDataBatchImpl(
|
|
354
|
-
checkpoint:
|
|
357
|
+
checkpoint: MongoSyncBucketStorageCheckpoint,
|
|
355
358
|
dataBuckets: storage.BucketDataRequest[],
|
|
356
359
|
options?: storage.BucketDataBatchOptions
|
|
357
360
|
): AsyncIterable<storage.SyncBucketDataChunk> {
|
|
@@ -528,7 +531,7 @@ export async function getParameterSetsV3(
|
|
|
528
531
|
|
|
529
532
|
export async function* getBucketDataBatchV3(
|
|
530
533
|
ctx: MongoSyncBucketStorageContextV3,
|
|
531
|
-
checkpoint:
|
|
534
|
+
checkpoint: MongoSyncBucketStorageCheckpoint,
|
|
532
535
|
dataBuckets: storage.BucketDataRequest[],
|
|
533
536
|
options?: storage.BucketDataBatchOptions
|
|
534
537
|
): AsyncIterable<storage.SyncBucketDataChunk> {
|
|
@@ -536,13 +539,25 @@ export async function* getBucketDataBatchV3(
|
|
|
536
539
|
return;
|
|
537
540
|
}
|
|
538
541
|
|
|
539
|
-
if (checkpoint == null) {
|
|
542
|
+
if (checkpoint.checkpoint == null) {
|
|
540
543
|
throw new Error('checkpoint is null');
|
|
541
544
|
}
|
|
542
545
|
|
|
546
|
+
const readPreference = options?.requestHint == 'bulk' ? ctx.readPreference : undefined;
|
|
547
|
+
const readConcern = ctx.readPreference == null ? undefined : 'majority';
|
|
548
|
+
const session =
|
|
549
|
+
readPreference == null || checkpoint.snapshotTime == null
|
|
550
|
+
? undefined
|
|
551
|
+
: ctx.db.client.startSession({ causalConsistency: true });
|
|
552
|
+
await using _ = { [Symbol.asyncDispose]: async () => session?.endSession() };
|
|
553
|
+
|
|
554
|
+
if (session != null) {
|
|
555
|
+
session.advanceOperationTime(checkpoint.snapshotTime);
|
|
556
|
+
}
|
|
557
|
+
|
|
543
558
|
const batchLimit = options?.limit ?? storage.DEFAULT_DOCUMENT_BATCH_LIMIT;
|
|
544
559
|
const chunkSizeLimitBytes = options?.chunkLimitBytes ?? storage.DEFAULT_DOCUMENT_CHUNK_LIMIT_BYTES;
|
|
545
|
-
const end = checkpoint;
|
|
560
|
+
const end = checkpoint.checkpoint;
|
|
546
561
|
let remainingLimit = batchLimit;
|
|
547
562
|
|
|
548
563
|
const requestsByDefinition = new Map<string, storage.BucketDataRequest[]>();
|
|
@@ -580,7 +595,9 @@ export async function* getBucketDataBatchV3(
|
|
|
580
595
|
// raw: true returns Buffers, but the driver typing doesn't reflect that
|
|
581
596
|
// without an explicit cast to FindCursor<Buffer>.
|
|
582
597
|
const cursor = collection.find(filter, {
|
|
583
|
-
session
|
|
598
|
+
session,
|
|
599
|
+
readPreference,
|
|
600
|
+
readConcern,
|
|
584
601
|
sort: { _id: 1 },
|
|
585
602
|
raw: true,
|
|
586
603
|
maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS,
|
package/src/types/types.ts
CHANGED
|
@@ -2,9 +2,25 @@ import * as lib_mongo from '@powersync/lib-service-mongodb';
|
|
|
2
2
|
import * as service_types from '@powersync/service-types';
|
|
3
3
|
import * as t from 'ts-codec';
|
|
4
4
|
|
|
5
|
+
export const MongoStorageReadPreference = t
|
|
6
|
+
.literal('primary')
|
|
7
|
+
.or(t.literal('primaryPreferred'))
|
|
8
|
+
.or(t.literal('secondary'))
|
|
9
|
+
.or(t.literal('secondaryPreferred'))
|
|
10
|
+
.or(t.literal('nearest'));
|
|
11
|
+
|
|
12
|
+
export type MongoStorageReadPreference = t.Encoded<typeof MongoStorageReadPreference>;
|
|
13
|
+
|
|
5
14
|
export const MongoStorageConfig = lib_mongo.BaseMongoConfig.and(
|
|
6
15
|
t.object({
|
|
7
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Read preference for bulk checksum and bucket data reads.
|
|
18
|
+
*
|
|
19
|
+
* If unset, MongoDB driver defaults are used for backwards compatibility.
|
|
20
|
+
*
|
|
21
|
+
* This is an experimental option, and may be removed in a future release.
|
|
22
|
+
*/
|
|
23
|
+
bulk_read_preference: MongoStorageReadPreference.optional()
|
|
8
24
|
})
|
|
9
25
|
);
|
|
10
26
|
|
|
@@ -95,7 +95,7 @@ bucket_definitions:
|
|
|
95
95
|
const users = ['u1', 'u2'];
|
|
96
96
|
const userRequests = users.map((user) => bucketRequest(syncRules, `by_user["${user}"]`));
|
|
97
97
|
const [u1Request, u2Request] = userRequests;
|
|
98
|
-
const checksumAfter = await bucketStorage.getChecksums(checkpoint, userRequests);
|
|
98
|
+
const checksumAfter = await bucketStorage.getChecksums(test_utils.testCheckpoint(checkpoint), userRequests);
|
|
99
99
|
expect(checksumAfter.get(u1Request.bucket)).toEqual({
|
|
100
100
|
bucket: u1Request.bucket,
|
|
101
101
|
checksum: -659469718,
|
|
@@ -151,7 +151,7 @@ bucket_definitions:
|
|
|
151
151
|
const users = ['u1', 'u2'];
|
|
152
152
|
const userRequests = users.map((user) => bucketRequest(syncRulesContent, `by_user2["${user}"]`));
|
|
153
153
|
const [u1Request, u2Request] = userRequests;
|
|
154
|
-
const checksumAfter = await bucketStorage.getChecksums(checkpoint, userRequests);
|
|
154
|
+
const checksumAfter = await bucketStorage.getChecksums(test_utils.testCheckpoint(checkpoint), userRequests);
|
|
155
155
|
expect(checksumAfter.get(u1Request.bucket)).toEqual({
|
|
156
156
|
bucket: u1Request.bucket,
|
|
157
157
|
checksum: -659469718,
|
|
@@ -869,7 +869,7 @@ bucket_definitions:
|
|
|
869
869
|
} as any
|
|
870
870
|
};
|
|
871
871
|
|
|
872
|
-
const result = await bucketStorage.getChecksums(60n, [request]);
|
|
872
|
+
const result = await bucketStorage.getChecksums(test_utils.testCheckpoint(60n), [request]);
|
|
873
873
|
const checksumResult = result.get(BUCKET)!;
|
|
874
874
|
|
|
875
875
|
// The total checksum should be: compacted (ops 10,20,30) + partial (ops 40,50,60)
|
|
@@ -922,7 +922,7 @@ bucket_definitions:
|
|
|
922
922
|
} as any
|
|
923
923
|
};
|
|
924
924
|
|
|
925
|
-
const result = await bucketStorage.getChecksums(45n, [request]);
|
|
925
|
+
const result = await bucketStorage.getChecksums(test_utils.testCheckpoint(45n), [request]);
|
|
926
926
|
const checksumResult = result.get(BUCKET)!;
|
|
927
927
|
|
|
928
928
|
// If createBucketFilter's _id.o <= 45 excludes this document,
|
|
@@ -192,7 +192,11 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
192
192
|
|
|
193
193
|
const options: storage.BucketDataBatchOptions = {};
|
|
194
194
|
const batch1 = await test_utils.fromAsync(
|
|
195
|
-
bucketStorage.getBucketDataBatch(
|
|
195
|
+
bucketStorage.getBucketDataBatch(
|
|
196
|
+
test_utils.testCheckpoint(checkpoint),
|
|
197
|
+
[bucketRequest(syncRulesContent, 'global[]', 0n)],
|
|
198
|
+
options
|
|
199
|
+
)
|
|
196
200
|
);
|
|
197
201
|
expect(test_utils.getBatchData(batch1)).toEqual([
|
|
198
202
|
{ op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 },
|
|
@@ -206,7 +210,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
206
210
|
|
|
207
211
|
const batch2 = await test_utils.fromAsync(
|
|
208
212
|
bucketStorage.getBucketDataBatch(
|
|
209
|
-
checkpoint,
|
|
213
|
+
test_utils.testCheckpoint(checkpoint),
|
|
210
214
|
[bucketRequest(syncRulesContent, 'global[]', batch1[0].chunkData.next_after)],
|
|
211
215
|
options
|
|
212
216
|
)
|
|
@@ -222,7 +226,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
222
226
|
|
|
223
227
|
const batch3 = await test_utils.fromAsync(
|
|
224
228
|
bucketStorage.getBucketDataBatch(
|
|
225
|
-
checkpoint,
|
|
229
|
+
test_utils.testCheckpoint(checkpoint),
|
|
226
230
|
[bucketRequest(syncRulesContent, 'global[]', batch2[0].chunkData.next_after)],
|
|
227
231
|
options
|
|
228
232
|
)
|
|
@@ -1348,6 +1352,44 @@ streams:
|
|
|
1348
1352
|
await syncRules.current_lock?.release();
|
|
1349
1353
|
});
|
|
1350
1354
|
|
|
1355
|
+
test.runIf(storageVersion >= 3)('does not lock when appending a sync config to an existing stream', async () => {
|
|
1356
|
+
await using factory = await storageConfig.factory();
|
|
1357
|
+
|
|
1358
|
+
const firstRules = `
|
|
1359
|
+
config:
|
|
1360
|
+
edition: 3
|
|
1361
|
+
|
|
1362
|
+
streams:
|
|
1363
|
+
by_owner:
|
|
1364
|
+
query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
|
|
1365
|
+
`;
|
|
1366
|
+
const secondRules = `
|
|
1367
|
+
config:
|
|
1368
|
+
edition: 3
|
|
1369
|
+
|
|
1370
|
+
streams:
|
|
1371
|
+
by_project:
|
|
1372
|
+
query: SELECT * FROM todos WHERE project_id = subscription.parameter('project_id')
|
|
1373
|
+
`;
|
|
1374
|
+
|
|
1375
|
+
const first = await factory.updateSyncRules(updateSyncRulesFromYaml(firstRules, { storageVersion, lock: true }));
|
|
1376
|
+
expect(first.current_lock?.sync_rules_id).toBe(first.replicationStreamId);
|
|
1377
|
+
try {
|
|
1378
|
+
const firstStorage = factory.getInstance(first);
|
|
1379
|
+
await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1380
|
+
await firstWriter.markAllSnapshotDone('1/1');
|
|
1381
|
+
await firstWriter.commit('1/1');
|
|
1382
|
+
|
|
1383
|
+
const second = await factory.updateSyncRules(
|
|
1384
|
+
updateSyncRulesFromYaml(secondRules, { storageVersion, lock: true })
|
|
1385
|
+
);
|
|
1386
|
+
expect(second.replicationStreamId).toBe(first.replicationStreamId);
|
|
1387
|
+
expect(second.current_lock).toBeNull();
|
|
1388
|
+
} finally {
|
|
1389
|
+
await first.current_lock?.release();
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1351
1393
|
test.runIf(storageVersion < 3)('uses a single current_data collection for v1 source records', async () => {
|
|
1352
1394
|
await using factory = await storageConfig.factory();
|
|
1353
1395
|
const syncRules = await factory.updateSyncRules(
|
|
@@ -1705,7 +1747,9 @@ describe('sync - mongodb', () => {
|
|
|
1705
1747
|
async function getFilteredOps(start: number, checkpoint: number): Promise<bigint[]> {
|
|
1706
1748
|
const { syncRules, bucketStorage } = await setupFilteringTest();
|
|
1707
1749
|
const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', BigInt(start));
|
|
1708
|
-
const batch = await test_utils.fromAsync(
|
|
1750
|
+
const batch = await test_utils.fromAsync(
|
|
1751
|
+
bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(BigInt(checkpoint)), [request])
|
|
1752
|
+
);
|
|
1709
1753
|
const ops = batch.flatMap((b) => b.chunkData.data.map((d) => BigInt(d.op_id)));
|
|
1710
1754
|
return ops;
|
|
1711
1755
|
}
|
|
@@ -1850,7 +1894,9 @@ describe('sync - mongodb', () => {
|
|
|
1850
1894
|
const roundRequests = requests
|
|
1851
1895
|
.filter((request) => pending.has(request.bucket))
|
|
1852
1896
|
.map((request) => ({ ...request, start: positions.get(request.bucket)! }));
|
|
1853
|
-
const batch = await test_utils.fromAsync(
|
|
1897
|
+
const batch = await test_utils.fromAsync(
|
|
1898
|
+
bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(end), roundRequests)
|
|
1899
|
+
);
|
|
1854
1900
|
let anyHasMore = false;
|
|
1855
1901
|
for (const { chunkData } of batch) {
|
|
1856
1902
|
positions.set(chunkData.bucket, BigInt(chunkData.next_after));
|
|
@@ -131,7 +131,9 @@ function registerStoreCurrentDataTests(storageVersion: number) {
|
|
|
131
131
|
const checkpoint = flushResult!.flushed_op;
|
|
132
132
|
|
|
133
133
|
const batch = await test_utils.fromAsync(
|
|
134
|
-
bucketStorage.getBucketDataBatch(checkpoint, [
|
|
134
|
+
bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(checkpoint), [
|
|
135
|
+
bucketRequest(syncRulesContent, 'global[]', 0n)
|
|
136
|
+
])
|
|
135
137
|
);
|
|
136
138
|
expect(test_utils.getBatchData(batch)).toMatchObject([{ op: 'PUT', object_id: 'test1' }]);
|
|
137
139
|
|
|
@@ -161,7 +163,9 @@ function registerStoreCurrentDataTests(storageVersion: number) {
|
|
|
161
163
|
const checkpoint = flushResult!.flushed_op;
|
|
162
164
|
|
|
163
165
|
const batch = await test_utils.fromAsync(
|
|
164
|
-
bucketStorage.getBucketDataBatch(checkpoint, [
|
|
166
|
+
bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(checkpoint), [
|
|
167
|
+
bucketRequest(syncRulesContent, 'global[]', 0n)
|
|
168
|
+
])
|
|
165
169
|
);
|
|
166
170
|
expect(test_utils.getBatchData(batch)).toMatchObject([{ op: 'PUT', object_id: 'test1' }]);
|
|
167
171
|
|
|
@@ -200,7 +204,9 @@ function registerStoreCurrentDataTests(storageVersion: number) {
|
|
|
200
204
|
const checkpoint = flushResult!.flushed_op;
|
|
201
205
|
|
|
202
206
|
const batch = await test_utils.fromAsync(
|
|
203
|
-
bucketStorage.getBucketDataBatch(checkpoint, [
|
|
207
|
+
bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(checkpoint), [
|
|
208
|
+
bucketRequest(syncRulesContent, 'global[]', 0n)
|
|
209
|
+
])
|
|
204
210
|
);
|
|
205
211
|
const data = test_utils.getBatchData(batch);
|
|
206
212
|
expect(data.length).toBeGreaterThan(0);
|