@powersync/service-core-tests 0.17.2 → 0.18.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.
- package/CHANGELOG.md +52 -0
- package/dist/test-utils/AbstractStreamTestContext.d.ts +1 -1
- package/dist/test-utils/AbstractStreamTestContext.js +2 -3
- package/dist/test-utils/AbstractStreamTestContext.js.map +1 -1
- package/dist/test-utils/StorageDataHelpers.d.ts +3 -2
- package/dist/test-utils/StorageDataHelpers.js +40 -2
- package/dist/test-utils/StorageDataHelpers.js.map +1 -1
- package/dist/test-utils/general-utils.d.ts +3 -1
- package/dist/test-utils/general-utils.js +13 -1
- package/dist/test-utils/general-utils.js.map +1 -1
- package/dist/test-utils/storage-combinations.d.ts +13 -0
- package/dist/test-utils/storage-combinations.js +26 -0
- package/dist/test-utils/storage-combinations.js.map +1 -0
- package/dist/test-utils/stream_utils.d.ts +0 -1
- package/dist/test-utils/stream_utils.js +0 -10
- package/dist/test-utils/stream_utils.js.map +1 -1
- package/dist/test-utils/test-utils-index.d.ts +1 -0
- package/dist/test-utils/test-utils-index.js +1 -0
- package/dist/test-utils/test-utils-index.js.map +1 -1
- package/dist/tests/register-compacting-tests.js +15 -15
- package/dist/tests/register-compacting-tests.js.map +1 -1
- package/dist/tests/register-data-storage-checkpoint-tests.js +132 -14
- package/dist/tests/register-data-storage-checkpoint-tests.js.map +1 -1
- package/dist/tests/register-data-storage-data-tests.js +116 -96
- package/dist/tests/register-data-storage-data-tests.js.map +1 -1
- package/dist/tests/register-parameter-compacting-tests.js +3 -2
- package/dist/tests/register-parameter-compacting-tests.js.map +1 -1
- package/dist/tests/register-sync-tests.js +267 -82
- package/dist/tests/register-sync-tests.js.map +1 -1
- package/dist/tests/util.d.ts +6 -0
- package/dist/tests/util.js +12 -0
- package/dist/tests/util.js.map +1 -1
- package/package.json +4 -4
- package/src/test-utils/AbstractStreamTestContext.ts +2 -3
- package/src/test-utils/StorageDataHelpers.ts +45 -2
- package/src/test-utils/general-utils.ts +27 -3
- package/src/test-utils/storage-combinations.ts +51 -0
- package/src/test-utils/stream_utils.ts +0 -11
- package/src/test-utils/test-utils-index.ts +1 -0
- package/src/tests/register-compacting-tests.ts +15 -15
- package/src/tests/register-data-storage-checkpoint-tests.ts +161 -14
- package/src/tests/register-data-storage-data-tests.ts +132 -134
- package/src/tests/register-parameter-compacting-tests.ts +3 -2
- package/src/tests/register-sync-tests.ts +288 -84
- package/src/tests/util.ts +13 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -12,9 +12,10 @@ import { JSONBig } from '@powersync/service-jsonbig';
|
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import * as timers from 'timers/promises';
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
15
|
-
import { expect, test } from 'vitest';
|
|
15
|
+
import { expect, test, vi } from 'vitest';
|
|
16
16
|
import * as test_utils from '../test-utils/test-utils-index.js';
|
|
17
17
|
import { bucketRequest, METRICS_HELPER } from '../test-utils/test-utils-index.js';
|
|
18
|
+
import { compactActive } from './util.js';
|
|
18
19
|
|
|
19
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
21
|
const __dirname = path.dirname(__filename);
|
|
@@ -251,29 +252,36 @@ streams:
|
|
|
251
252
|
expect(lines).toMatchSnapshot();
|
|
252
253
|
});
|
|
253
254
|
|
|
254
|
-
test('
|
|
255
|
+
test('carries pending low-priority buckets into a new checkpoint', async () => {
|
|
255
256
|
await using f = await factory();
|
|
256
257
|
|
|
257
258
|
const syncRules = await updateSyncRules(f, {
|
|
258
259
|
content: `
|
|
259
260
|
bucket_definitions:
|
|
260
|
-
|
|
261
|
+
b0a:
|
|
261
262
|
priority: 2
|
|
262
263
|
data:
|
|
263
|
-
- SELECT * FROM test WHERE
|
|
264
|
+
- SELECT * FROM test WHERE substring(id, 1, 6) = 'first-';
|
|
265
|
+
b0b:
|
|
266
|
+
priority: 3
|
|
267
|
+
data:
|
|
268
|
+
- SELECT * FROM test WHERE substring(id, 1, 4) = 'low-';
|
|
264
269
|
b1:
|
|
265
270
|
priority: 1
|
|
266
271
|
data:
|
|
267
|
-
- SELECT * FROM test WHERE
|
|
272
|
+
- SELECT * FROM test WHERE substring(id, 1, 8) = 'highprio';
|
|
268
273
|
`
|
|
269
274
|
});
|
|
270
275
|
|
|
271
276
|
const bucketStorage = f.getInstance(syncRules);
|
|
272
277
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
273
278
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
279
|
+
const syncRulesContent = syncRules.syncConfigContent[0];
|
|
280
|
+
const b0aBucket = test_utils.bucketRequest(syncRulesContent, 'b0a[]').bucket;
|
|
281
|
+
const b0bBucket = test_utils.bucketRequest(syncRulesContent, 'b0b[]').bucket;
|
|
282
|
+
const b1Bucket = test_utils.bucketRequest(syncRulesContent, 'b1[]').bucket;
|
|
274
283
|
|
|
275
284
|
await writer.markAllSnapshotDone('0/1');
|
|
276
|
-
// Initial data: Add one priority row and 10k low-priority rows.
|
|
277
285
|
await writer.save({
|
|
278
286
|
sourceTable: testTable,
|
|
279
287
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -283,15 +291,26 @@ bucket_definitions:
|
|
|
283
291
|
},
|
|
284
292
|
afterReplicaId: 'highprio'
|
|
285
293
|
});
|
|
286
|
-
for (let i = 0; i <
|
|
294
|
+
for (let i = 0; i < 999; i++) {
|
|
287
295
|
await writer.save({
|
|
288
296
|
sourceTable: testTable,
|
|
289
297
|
tag: storage.SaveOperationTag.INSERT,
|
|
290
298
|
after: {
|
|
291
|
-
id:
|
|
292
|
-
description: 'low
|
|
299
|
+
id: `first-${i}`,
|
|
300
|
+
description: 'first low-priority bucket'
|
|
293
301
|
},
|
|
294
|
-
afterReplicaId:
|
|
302
|
+
afterReplicaId: `first-${i}`
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
for (let i = 0; i < 9_001; i++) {
|
|
306
|
+
await writer.save({
|
|
307
|
+
sourceTable: testTable,
|
|
308
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
309
|
+
after: {
|
|
310
|
+
id: `low-${i}`,
|
|
311
|
+
description: 'last low-priority bucket'
|
|
312
|
+
},
|
|
313
|
+
afterReplicaId: `low-${i}`
|
|
295
314
|
});
|
|
296
315
|
}
|
|
297
316
|
|
|
@@ -311,8 +330,12 @@ bucket_definitions:
|
|
|
311
330
|
isEncodingAsBson: false
|
|
312
331
|
});
|
|
313
332
|
|
|
314
|
-
|
|
315
|
-
|
|
333
|
+
const dataLines: Array<{ bucket: string; objectIds: string[]; afterCheckpointDiff: boolean }> = [];
|
|
334
|
+
const partialCheckpoints: Array<{ priority: number; afterCheckpointDiff: boolean }> = [];
|
|
335
|
+
const checkpoints: any[] = [];
|
|
336
|
+
const checkpointCompletes: any[] = [];
|
|
337
|
+
let afterCheckpointDiff = false;
|
|
338
|
+
let interruptionTriggered = false;
|
|
316
339
|
|
|
317
340
|
for await (let next of stream) {
|
|
318
341
|
if (typeof next == 'string') {
|
|
@@ -320,10 +343,13 @@ bucket_definitions:
|
|
|
320
343
|
}
|
|
321
344
|
if (typeof next === 'object' && next !== null) {
|
|
322
345
|
if ('partial_checkpoint_complete' in next) {
|
|
323
|
-
|
|
324
|
-
|
|
346
|
+
partialCheckpoints.push({
|
|
347
|
+
priority: next.partial_checkpoint_complete.priority,
|
|
348
|
+
afterCheckpointDiff
|
|
349
|
+
});
|
|
325
350
|
|
|
326
|
-
|
|
351
|
+
if (next.partial_checkpoint_complete.priority == 2 && !interruptionTriggered) {
|
|
352
|
+
interruptionTriggered = true;
|
|
327
353
|
await writer.save({
|
|
328
354
|
sourceTable: testTable,
|
|
329
355
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -335,28 +361,76 @@ bucket_definitions:
|
|
|
335
361
|
});
|
|
336
362
|
|
|
337
363
|
await writer.commit('0/2');
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// 1000 low-priority items were synchronized.
|
|
341
|
-
expect(sentCheckpoints).toBe(2);
|
|
342
|
-
expect(sentRows).toBeGreaterThan(1000);
|
|
364
|
+
// Let the checkpoint watcher observe the update before requesting the next priority.
|
|
365
|
+
await timers.setTimeout(50);
|
|
343
366
|
}
|
|
344
367
|
}
|
|
345
368
|
if ('checkpoint' in next || 'checkpoint_diff' in next) {
|
|
346
|
-
|
|
369
|
+
checkpoints.push(next);
|
|
370
|
+
afterCheckpointDiff = 'checkpoint_diff' in next;
|
|
347
371
|
}
|
|
348
372
|
|
|
349
373
|
if ('data' in next) {
|
|
350
|
-
|
|
374
|
+
dataLines.push({
|
|
375
|
+
bucket: next.data.bucket,
|
|
376
|
+
objectIds: next.data.data.flatMap((entry: any) => (entry.object_id == null ? [] : [entry.object_id])),
|
|
377
|
+
afterCheckpointDiff
|
|
378
|
+
});
|
|
351
379
|
}
|
|
352
380
|
if ('checkpoint_complete' in next) {
|
|
381
|
+
checkpointCompletes.push(next);
|
|
353
382
|
break;
|
|
354
383
|
}
|
|
355
384
|
}
|
|
356
385
|
}
|
|
357
386
|
|
|
358
|
-
|
|
359
|
-
|
|
387
|
+
// Expected flow (data may be split into any number of chunks):
|
|
388
|
+
//
|
|
389
|
+
// checkpoint
|
|
390
|
+
// data: b1 contains highprio
|
|
391
|
+
// partial_checkpoint_complete: priority 1
|
|
392
|
+
// data: b0a contains all 999 first-* rows (1,000 operations including highprio)
|
|
393
|
+
// partial_checkpoint_complete: priority 2
|
|
394
|
+
// ## add highprio2, interrupting before b0b starts
|
|
395
|
+
// checkpoint_diff
|
|
396
|
+
// data: b1 contains highprio2
|
|
397
|
+
// partial_checkpoint_complete: priority 1
|
|
398
|
+
// data: b0b contains all 9,001 low-* rows carried over from the interrupted checkpoint
|
|
399
|
+
// checkpoint_complete: only for the new checkpoint
|
|
400
|
+
expect(interruptionTriggered).toBe(true);
|
|
401
|
+
expect(checkpoints).toHaveLength(2);
|
|
402
|
+
expect(checkpoints[0]).toHaveProperty('checkpoint');
|
|
403
|
+
expect(checkpoints[1]).toHaveProperty('checkpoint_diff');
|
|
404
|
+
expect(partialCheckpoints).toEqual([
|
|
405
|
+
{ priority: 1, afterCheckpointDiff: false },
|
|
406
|
+
{ priority: 2, afterCheckpointDiff: false },
|
|
407
|
+
{ priority: 1, afterCheckpointDiff: true }
|
|
408
|
+
]);
|
|
409
|
+
expect(checkpointCompletes).toEqual([
|
|
410
|
+
{
|
|
411
|
+
checkpoint_complete: {
|
|
412
|
+
last_op_id: checkpoints[1].checkpoint_diff.last_op_id
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
]);
|
|
416
|
+
|
|
417
|
+
const objectIdsFor = (bucket: string, afterDiff: boolean) =>
|
|
418
|
+
dataLines
|
|
419
|
+
.filter((line) => line.bucket == bucket && line.afterCheckpointDiff == afterDiff)
|
|
420
|
+
.flatMap((line) => line.objectIds);
|
|
421
|
+
|
|
422
|
+
expect(objectIdsFor(b1Bucket, false)).toEqual(['highprio']);
|
|
423
|
+
const b0aObjectIds = objectIdsFor(b0aBucket, false);
|
|
424
|
+
expect(new Set(b0aObjectIds)).toEqual(new Set(Array.from({ length: 999 }, (_, i) => `first-${i}`)));
|
|
425
|
+
expect(b0aObjectIds).toHaveLength(999);
|
|
426
|
+
expect(objectIdsFor(b0bBucket, false)).toEqual([]);
|
|
427
|
+
|
|
428
|
+
expect(objectIdsFor(b1Bucket, true)).toEqual(['highprio2']);
|
|
429
|
+
expect(objectIdsFor(b0aBucket, true)).toEqual([]);
|
|
430
|
+
|
|
431
|
+
const b0bObjectIds = objectIdsFor(b0bBucket, true);
|
|
432
|
+
expect(new Set(b0bObjectIds)).toEqual(new Set(Array.from({ length: 9_001 }, (_, i) => `low-${i}`)));
|
|
433
|
+
expect(b0bObjectIds).toHaveLength(9_001);
|
|
360
434
|
});
|
|
361
435
|
|
|
362
436
|
test('sync interruptions with unrelated data', async () => {
|
|
@@ -490,37 +564,36 @@ bucket_definitions:
|
|
|
490
564
|
expect(sentRows).toBe(10002);
|
|
491
565
|
});
|
|
492
566
|
|
|
493
|
-
test('
|
|
567
|
+
test('restarts updated low-priority buckets at a new checkpoint', async () => {
|
|
494
568
|
await using f = await factory();
|
|
495
569
|
|
|
496
|
-
// bucket0a -> send all data
|
|
497
|
-
// then interrupt checkpoint with new data for all buckets
|
|
498
|
-
// -> data for all buckets should be sent in the new checkpoint
|
|
499
|
-
|
|
500
570
|
const syncRules = await updateSyncRules(f, {
|
|
501
571
|
content: `
|
|
502
572
|
bucket_definitions:
|
|
503
573
|
b0a:
|
|
504
574
|
priority: 2
|
|
505
575
|
data:
|
|
506
|
-
- SELECT * FROM test WHERE
|
|
576
|
+
- SELECT * FROM test WHERE substring(id, 1, 6) = 'first-';
|
|
507
577
|
b0b:
|
|
508
|
-
priority:
|
|
578
|
+
priority: 3
|
|
509
579
|
data:
|
|
510
|
-
- SELECT * FROM test WHERE
|
|
580
|
+
- SELECT * FROM test WHERE substring(id, 1, 4) = 'low-';
|
|
511
581
|
b1:
|
|
512
582
|
priority: 1
|
|
513
583
|
data:
|
|
514
|
-
- SELECT * FROM test WHERE
|
|
584
|
+
- SELECT * FROM test WHERE substring(id, 1, 8) = 'highprio';
|
|
515
585
|
`
|
|
516
586
|
});
|
|
517
587
|
|
|
518
588
|
const bucketStorage = f.getInstance(syncRules);
|
|
519
589
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
520
590
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
591
|
+
const syncRulesContent = syncRules.syncConfigContent[0];
|
|
592
|
+
const b0aBucket = test_utils.bucketRequest(syncRulesContent, 'b0a[]').bucket;
|
|
593
|
+
const b0bBucket = test_utils.bucketRequest(syncRulesContent, 'b0b[]').bucket;
|
|
594
|
+
const b1Bucket = test_utils.bucketRequest(syncRulesContent, 'b1[]').bucket;
|
|
521
595
|
|
|
522
596
|
await writer.markAllSnapshotDone('0/1');
|
|
523
|
-
// Initial data: Add one priority row and 10k low-priority rows.
|
|
524
597
|
await writer.save({
|
|
525
598
|
sourceTable: testTable,
|
|
526
599
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -530,15 +603,26 @@ bucket_definitions:
|
|
|
530
603
|
},
|
|
531
604
|
afterReplicaId: 'highprio'
|
|
532
605
|
});
|
|
606
|
+
for (let i = 0; i < 999; i++) {
|
|
607
|
+
await writer.save({
|
|
608
|
+
sourceTable: testTable,
|
|
609
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
610
|
+
after: {
|
|
611
|
+
id: `first-${i}`,
|
|
612
|
+
description: 'first low-priority bucket'
|
|
613
|
+
},
|
|
614
|
+
afterReplicaId: `first-${i}`
|
|
615
|
+
});
|
|
616
|
+
}
|
|
533
617
|
for (let i = 0; i < 2_000; i++) {
|
|
534
618
|
await writer.save({
|
|
535
619
|
sourceTable: testTable,
|
|
536
620
|
tag: storage.SaveOperationTag.INSERT,
|
|
537
621
|
after: {
|
|
538
|
-
id:
|
|
622
|
+
id: `low-${i}`,
|
|
539
623
|
description: 'low prio'
|
|
540
624
|
},
|
|
541
|
-
afterReplicaId:
|
|
625
|
+
afterReplicaId: `low-${i}`
|
|
542
626
|
});
|
|
543
627
|
}
|
|
544
628
|
|
|
@@ -558,8 +642,12 @@ bucket_definitions:
|
|
|
558
642
|
isEncodingAsBson: false
|
|
559
643
|
});
|
|
560
644
|
|
|
561
|
-
|
|
562
|
-
|
|
645
|
+
const dataLines: Array<{ bucket: string; objectIds: string[]; afterCheckpointDiff: boolean }> = [];
|
|
646
|
+
const partialCheckpoints: Array<{ priority: number; afterCheckpointDiff: boolean }> = [];
|
|
647
|
+
const checkpoints: any[] = [];
|
|
648
|
+
const checkpointCompletes: any[] = [];
|
|
649
|
+
let afterCheckpointDiff = false;
|
|
650
|
+
let interruptionTriggered = false;
|
|
563
651
|
|
|
564
652
|
for await (let next of stream) {
|
|
565
653
|
if (typeof next == 'string') {
|
|
@@ -567,19 +655,13 @@ bucket_definitions:
|
|
|
567
655
|
}
|
|
568
656
|
if (typeof next === 'object' && next !== null) {
|
|
569
657
|
if ('partial_checkpoint_complete' in next) {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
658
|
+
partialCheckpoints.push({
|
|
659
|
+
priority: next.partial_checkpoint_complete.priority,
|
|
660
|
+
afterCheckpointDiff
|
|
661
|
+
});
|
|
575
662
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
sentRows += next.data.data.length;
|
|
579
|
-
|
|
580
|
-
if (sentRows == 1001) {
|
|
581
|
-
// Save new data to interrupt the low-priority sync.
|
|
582
|
-
// Add another high-priority row. This should interrupt the long-running low-priority sync.
|
|
663
|
+
if (next.partial_checkpoint_complete.priority == 2 && !interruptionTriggered) {
|
|
664
|
+
interruptionTriggered = true;
|
|
583
665
|
await writer.save({
|
|
584
666
|
sourceTable: testTable,
|
|
585
667
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -590,51 +672,87 @@ bucket_definitions:
|
|
|
590
672
|
afterReplicaId: 'highprio2'
|
|
591
673
|
});
|
|
592
674
|
|
|
593
|
-
// Also add a low-priority row
|
|
594
675
|
await writer.save({
|
|
595
676
|
sourceTable: testTable,
|
|
596
677
|
tag: storage.SaveOperationTag.INSERT,
|
|
597
678
|
after: {
|
|
598
|
-
id: '
|
|
679
|
+
id: 'low-2000',
|
|
599
680
|
description: 'Another low-priority row'
|
|
600
681
|
},
|
|
601
|
-
afterReplicaId: '
|
|
682
|
+
afterReplicaId: 'low-2000'
|
|
602
683
|
});
|
|
603
684
|
|
|
604
685
|
await writer.commit('0/2');
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
if (sentRows >= 1000 && sentRows <= 2001) {
|
|
608
|
-
// pause for a bit to give the stream time to process interruptions.
|
|
609
|
-
// This covers the data batch above and the next one.
|
|
686
|
+
// Let the checkpoint watcher observe the update before requesting the next priority.
|
|
610
687
|
await timers.setTimeout(50);
|
|
611
688
|
}
|
|
612
689
|
}
|
|
690
|
+
if ('checkpoint' in next || 'checkpoint_diff' in next) {
|
|
691
|
+
checkpoints.push(next);
|
|
692
|
+
afterCheckpointDiff = 'checkpoint_diff' in next;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if ('data' in next) {
|
|
696
|
+
dataLines.push({
|
|
697
|
+
bucket: next.data.bucket,
|
|
698
|
+
objectIds: next.data.data.flatMap((entry: any) => (entry.object_id == null ? [] : [entry.object_id])),
|
|
699
|
+
afterCheckpointDiff
|
|
700
|
+
});
|
|
701
|
+
}
|
|
613
702
|
if ('checkpoint_complete' in next) {
|
|
614
|
-
|
|
703
|
+
checkpointCompletes.push(next);
|
|
615
704
|
break;
|
|
616
705
|
}
|
|
617
706
|
}
|
|
618
707
|
}
|
|
619
708
|
|
|
620
|
-
// Expected
|
|
709
|
+
// Expected flow (data may be split into any number of chunks):
|
|
621
710
|
//
|
|
622
|
-
// checkpoint
|
|
623
|
-
// data
|
|
624
|
-
// partial_checkpoint_complete
|
|
625
|
-
// data (
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
// checkpoint_diff
|
|
629
|
-
// data
|
|
630
|
-
// partial_checkpoint_complete
|
|
631
|
-
// data
|
|
632
|
-
//
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
expect(
|
|
637
|
-
expect(
|
|
711
|
+
// checkpoint
|
|
712
|
+
// data: b1 contains highprio
|
|
713
|
+
// partial_checkpoint_complete: priority 1
|
|
714
|
+
// data: b0a contains all 999 first-* rows (1,000 operations including highprio)
|
|
715
|
+
// partial_checkpoint_complete: priority 2
|
|
716
|
+
// ## add highprio2 and low-2000, interrupting before b0b starts
|
|
717
|
+
// checkpoint_diff
|
|
718
|
+
// data: b1 contains highprio2
|
|
719
|
+
// partial_checkpoint_complete: priority 1
|
|
720
|
+
// data: b0b contains all 2,001 low-* rows
|
|
721
|
+
// checkpoint_complete: only for the new checkpoint
|
|
722
|
+
expect(interruptionTriggered).toBe(true);
|
|
723
|
+
expect(checkpoints).toHaveLength(2);
|
|
724
|
+
expect(checkpoints[0]).toHaveProperty('checkpoint');
|
|
725
|
+
expect(checkpoints[1]).toHaveProperty('checkpoint_diff');
|
|
726
|
+
expect(partialCheckpoints).toEqual([
|
|
727
|
+
{ priority: 1, afterCheckpointDiff: false },
|
|
728
|
+
{ priority: 2, afterCheckpointDiff: false },
|
|
729
|
+
{ priority: 1, afterCheckpointDiff: true }
|
|
730
|
+
]);
|
|
731
|
+
expect(checkpointCompletes).toEqual([
|
|
732
|
+
{
|
|
733
|
+
checkpoint_complete: {
|
|
734
|
+
last_op_id: checkpoints[1].checkpoint_diff.last_op_id
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
]);
|
|
738
|
+
|
|
739
|
+
const objectIdsFor = (bucket: string, afterDiff: boolean) =>
|
|
740
|
+
dataLines
|
|
741
|
+
.filter((line) => line.bucket == bucket && line.afterCheckpointDiff == afterDiff)
|
|
742
|
+
.flatMap((line) => line.objectIds);
|
|
743
|
+
|
|
744
|
+
expect(objectIdsFor(b1Bucket, false)).toEqual(['highprio']);
|
|
745
|
+
const b0aObjectIds = objectIdsFor(b0aBucket, false);
|
|
746
|
+
expect(new Set(b0aObjectIds)).toEqual(new Set(Array.from({ length: 999 }, (_, i) => `first-${i}`)));
|
|
747
|
+
expect(b0aObjectIds).toHaveLength(999);
|
|
748
|
+
expect(objectIdsFor(b0bBucket, false)).toEqual([]);
|
|
749
|
+
|
|
750
|
+
expect(objectIdsFor(b1Bucket, true)).toEqual(['highprio2']);
|
|
751
|
+
expect(objectIdsFor(b0aBucket, true)).toEqual([]);
|
|
752
|
+
|
|
753
|
+
const b0bObjectIds = objectIdsFor(b0bBucket, true);
|
|
754
|
+
expect(new Set(b0bObjectIds)).toEqual(new Set(Array.from({ length: 2_001 }, (_, i) => `low-${i}`)));
|
|
755
|
+
expect(b0bObjectIds).toHaveLength(2_001);
|
|
638
756
|
});
|
|
639
757
|
|
|
640
758
|
test('sends checkpoint complete line for empty checkpoint', async () => {
|
|
@@ -1090,6 +1208,92 @@ bucket_definitions:
|
|
|
1090
1208
|
expect(expLines).toMatchSnapshot();
|
|
1091
1209
|
});
|
|
1092
1210
|
|
|
1211
|
+
test('checksum invalidation skips the candidate checkpoint', async (context) => {
|
|
1212
|
+
await using f = await factory();
|
|
1213
|
+
|
|
1214
|
+
const syncRules = await updateSyncRules(f, {
|
|
1215
|
+
content: BASIC_SYNC_RULES
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
const bucketStorage = await f.getInstance(syncRules);
|
|
1219
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1220
|
+
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
1221
|
+
|
|
1222
|
+
await writer.markAllSnapshotDone('0/1');
|
|
1223
|
+
await writer.save({
|
|
1224
|
+
sourceTable: testTable,
|
|
1225
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
1226
|
+
after: {
|
|
1227
|
+
id: 't1',
|
|
1228
|
+
description: 'Test 1'
|
|
1229
|
+
},
|
|
1230
|
+
afterReplicaId: 't1'
|
|
1231
|
+
});
|
|
1232
|
+
await writer.commit('0/1');
|
|
1233
|
+
|
|
1234
|
+
let resolveInvalidatedChecksum!: () => void;
|
|
1235
|
+
const invalidatedChecksum = new Promise<void>((resolve) => {
|
|
1236
|
+
resolveInvalidatedChecksum = resolve;
|
|
1237
|
+
});
|
|
1238
|
+
const checksumSpy = vi.spyOn(bucketStorage, 'getChecksums').mockImplementationOnce(async (checkpoint, buckets) => {
|
|
1239
|
+
resolveInvalidatedChecksum();
|
|
1240
|
+
throw new storage.CheckpointChecksumInvalidatedError(checkpoint.checkpoint, buckets[0].bucket);
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
const stream = sync.streamResponse({
|
|
1244
|
+
syncContext,
|
|
1245
|
+
bucketStorage,
|
|
1246
|
+
syncRules: bucketStorage.getParsedSyncRules(test_utils.PARSE_OPTIONS),
|
|
1247
|
+
params: {
|
|
1248
|
+
buckets: [],
|
|
1249
|
+
include_checksum: true,
|
|
1250
|
+
raw_data: true
|
|
1251
|
+
},
|
|
1252
|
+
tracker,
|
|
1253
|
+
token: new JwtPayload({ sub: '', exp: Date.now() / 1000 + 10 }),
|
|
1254
|
+
isEncodingAsBson: false
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
const iter = stream[Symbol.asyncIterator]();
|
|
1258
|
+
context.onTestFinished(() => {
|
|
1259
|
+
iter.return?.();
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
const linesPromise = getCheckpointLines(iter, { consume: true });
|
|
1263
|
+
await invalidatedChecksum;
|
|
1264
|
+
|
|
1265
|
+
// The first candidate was discarded before CheckpointLine.advance(). A later
|
|
1266
|
+
// checkpoint must therefore be calculated from the original connection state.
|
|
1267
|
+
await writer.save({
|
|
1268
|
+
sourceTable: testTable,
|
|
1269
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
1270
|
+
after: {
|
|
1271
|
+
id: 't2',
|
|
1272
|
+
description: 'Test 2'
|
|
1273
|
+
},
|
|
1274
|
+
afterReplicaId: 't2'
|
|
1275
|
+
});
|
|
1276
|
+
await writer.commit('0/2');
|
|
1277
|
+
|
|
1278
|
+
const lines = await linesPromise;
|
|
1279
|
+
expect(checksumSpy).toHaveBeenCalledTimes(2);
|
|
1280
|
+
expect(lines[0]).toEqual({
|
|
1281
|
+
checkpoint: expect.objectContaining({
|
|
1282
|
+
last_op_id: '2'
|
|
1283
|
+
})
|
|
1284
|
+
});
|
|
1285
|
+
expect(lines).not.toContainEqual({
|
|
1286
|
+
checkpoint: expect.objectContaining({
|
|
1287
|
+
last_op_id: '1'
|
|
1288
|
+
})
|
|
1289
|
+
});
|
|
1290
|
+
expect(lines.at(-1)).toEqual({
|
|
1291
|
+
checkpoint_complete: expect.objectContaining({
|
|
1292
|
+
last_op_id: '2'
|
|
1293
|
+
})
|
|
1294
|
+
});
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1093
1297
|
test('compacting data - invalidate checkpoint', async (context) => {
|
|
1094
1298
|
// This tests a case of a compact operation invalidating a checkpoint in the
|
|
1095
1299
|
// middle of syncing data.
|
|
@@ -1105,6 +1309,7 @@ bucket_definitions:
|
|
|
1105
1309
|
const bucketStorage = await f.getInstance(syncRules);
|
|
1106
1310
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1107
1311
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
1312
|
+
const bucket = bucketRequest(syncRules.syncConfigContent[0], 'mybucket[]').bucket;
|
|
1108
1313
|
|
|
1109
1314
|
await writer.markAllSnapshotDone('0/1');
|
|
1110
1315
|
await writer.save({
|
|
@@ -1183,10 +1388,7 @@ bucket_definitions:
|
|
|
1183
1388
|
|
|
1184
1389
|
await writer.commit('0/2');
|
|
1185
1390
|
|
|
1186
|
-
await
|
|
1187
|
-
minBucketChanges: 1,
|
|
1188
|
-
minChangeRatio: 0
|
|
1189
|
-
});
|
|
1391
|
+
await compactActive(f, { compactBuckets: [bucket] });
|
|
1190
1392
|
|
|
1191
1393
|
const lines2 = await getCheckpointLines(iter, { consume: true });
|
|
1192
1394
|
|
|
@@ -1267,6 +1469,7 @@ bucket_definitions:
|
|
|
1267
1469
|
const bucketStorage = await f.getInstance(syncRules);
|
|
1268
1470
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1269
1471
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
1472
|
+
const highPriorityBucket = bucketRequest(syncRules.syncConfigContent[0], 'high_priority[]').bucket;
|
|
1270
1473
|
|
|
1271
1474
|
await writer.markAllSnapshotDone('0/1');
|
|
1272
1475
|
await writer.save({
|
|
@@ -1349,9 +1552,10 @@ bucket_definitions:
|
|
|
1349
1552
|
});
|
|
1350
1553
|
await writer.commit('0/2');
|
|
1351
1554
|
|
|
1352
|
-
await
|
|
1353
|
-
|
|
1354
|
-
|
|
1555
|
+
await compactActive(f, {
|
|
1556
|
+
// Explicitly compact the high-priority bucket: V3 schedules background
|
|
1557
|
+
// compaction, while this test needs compaction at this exact point.
|
|
1558
|
+
compactBuckets: [highPriorityBucket]
|
|
1355
1559
|
});
|
|
1356
1560
|
|
|
1357
1561
|
const lines = await getCheckpointLines(iter, { consume: true });
|
|
@@ -1403,7 +1607,7 @@ bucket_definitions:
|
|
|
1403
1607
|
heads: { '1': '1/0' }
|
|
1404
1608
|
}
|
|
1405
1609
|
])
|
|
1406
|
-
).get('test')!;
|
|
1610
|
+
).writeCheckpoints.get('test')!;
|
|
1407
1611
|
|
|
1408
1612
|
const params: sync.SyncStreamParameters = {
|
|
1409
1613
|
syncContext,
|
package/src/tests/util.ts
CHANGED
|
@@ -8,6 +8,19 @@ import {
|
|
|
8
8
|
} from '@powersync/service-sync-rules';
|
|
9
9
|
import { bucketRequest } from '../test-utils/general-utils.js';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Resolve storage again after test writes activate the sync config. The storage
|
|
13
|
+
* instance used by the writer retains its original PROCESSING stream snapshot,
|
|
14
|
+
* while compact() expects an instance constructed with the current state.
|
|
15
|
+
*/
|
|
16
|
+
export async function compactActive(factory: storage.BucketStorageFactory, options: storage.CompactOptions) {
|
|
17
|
+
const active = await factory.getActiveSyncConfig();
|
|
18
|
+
if (active == null) {
|
|
19
|
+
throw new Error('Expected an active sync config before compacting');
|
|
20
|
+
}
|
|
21
|
+
await active.storage.compact(options);
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
export function bucketRequestMap(
|
|
12
25
|
syncRules: storage.PersistedSyncConfigContent,
|
|
13
26
|
buckets: Iterable<readonly [string, bigint]>
|