@powersync/service-core-tests 0.17.1 → 0.18.0
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 +36 -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/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/tests/register-compacting-tests.js +7 -7
- package/dist/tests/register-compacting-tests.js.map +1 -1
- package/dist/tests/register-data-storage-checkpoint-tests.js +96 -3
- package/dist/tests/register-data-storage-checkpoint-tests.js.map +1 -1
- package/dist/tests/register-data-storage-data-tests.js +113 -94
- package/dist/tests/register-data-storage-data-tests.js.map +1 -1
- package/dist/tests/register-sync-tests.js +259 -75
- package/dist/tests/register-sync-tests.js.map +1 -1
- package/package.json +5 -5
- 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/stream_utils.ts +0 -11
- package/src/tests/register-compacting-tests.ts +7 -7
- package/src/tests/register-data-storage-checkpoint-tests.ts +122 -3
- package/src/tests/register-data-storage-data-tests.ts +129 -132
- package/src/tests/register-sync-tests.ts +280 -77
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -3,7 +3,7 @@ import { JSONBig } from '@powersync/service-jsonbig';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import * as timers from 'timers/promises';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import { expect, test } from 'vitest';
|
|
6
|
+
import { expect, test, vi } from 'vitest';
|
|
7
7
|
import * as test_utils from '../test-utils/test-utils-index.js';
|
|
8
8
|
import { bucketRequest, METRICS_HELPER } from '../test-utils/test-utils-index.js';
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -205,26 +205,33 @@ streams:
|
|
|
205
205
|
const lines = await consumeCheckpointLines(stream);
|
|
206
206
|
expect(lines).toMatchSnapshot();
|
|
207
207
|
});
|
|
208
|
-
test('
|
|
208
|
+
test('carries pending low-priority buckets into a new checkpoint', async () => {
|
|
209
209
|
await using f = await factory();
|
|
210
210
|
const syncRules = await updateSyncRules(f, {
|
|
211
211
|
content: `
|
|
212
212
|
bucket_definitions:
|
|
213
|
-
|
|
213
|
+
b0a:
|
|
214
214
|
priority: 2
|
|
215
215
|
data:
|
|
216
|
-
- SELECT * FROM test WHERE
|
|
216
|
+
- SELECT * FROM test WHERE substring(id, 1, 6) = 'first-';
|
|
217
|
+
b0b:
|
|
218
|
+
priority: 3
|
|
219
|
+
data:
|
|
220
|
+
- SELECT * FROM test WHERE substring(id, 1, 4) = 'low-';
|
|
217
221
|
b1:
|
|
218
222
|
priority: 1
|
|
219
223
|
data:
|
|
220
|
-
- SELECT * FROM test WHERE
|
|
224
|
+
- SELECT * FROM test WHERE substring(id, 1, 8) = 'highprio';
|
|
221
225
|
`
|
|
222
226
|
});
|
|
223
227
|
const bucketStorage = f.getInstance(syncRules);
|
|
224
228
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
225
229
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
230
|
+
const syncRulesContent = syncRules.syncConfigContent[0];
|
|
231
|
+
const b0aBucket = test_utils.bucketRequest(syncRulesContent, 'b0a[]').bucket;
|
|
232
|
+
const b0bBucket = test_utils.bucketRequest(syncRulesContent, 'b0b[]').bucket;
|
|
233
|
+
const b1Bucket = test_utils.bucketRequest(syncRulesContent, 'b1[]').bucket;
|
|
226
234
|
await writer.markAllSnapshotDone('0/1');
|
|
227
|
-
// Initial data: Add one priority row and 10k low-priority rows.
|
|
228
235
|
await writer.save({
|
|
229
236
|
sourceTable: testTable,
|
|
230
237
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -234,15 +241,26 @@ bucket_definitions:
|
|
|
234
241
|
},
|
|
235
242
|
afterReplicaId: 'highprio'
|
|
236
243
|
});
|
|
237
|
-
for (let i = 0; i <
|
|
244
|
+
for (let i = 0; i < 999; i++) {
|
|
238
245
|
await writer.save({
|
|
239
246
|
sourceTable: testTable,
|
|
240
247
|
tag: storage.SaveOperationTag.INSERT,
|
|
241
248
|
after: {
|
|
242
|
-
id:
|
|
243
|
-
description: 'low
|
|
249
|
+
id: `first-${i}`,
|
|
250
|
+
description: 'first low-priority bucket'
|
|
244
251
|
},
|
|
245
|
-
afterReplicaId:
|
|
252
|
+
afterReplicaId: `first-${i}`
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
for (let i = 0; i < 9_001; i++) {
|
|
256
|
+
await writer.save({
|
|
257
|
+
sourceTable: testTable,
|
|
258
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
259
|
+
after: {
|
|
260
|
+
id: `low-${i}`,
|
|
261
|
+
description: 'last low-priority bucket'
|
|
262
|
+
},
|
|
263
|
+
afterReplicaId: `low-${i}`
|
|
246
264
|
});
|
|
247
265
|
}
|
|
248
266
|
await writer.commit('0/1');
|
|
@@ -259,17 +277,24 @@ bucket_definitions:
|
|
|
259
277
|
token: new JwtPayload({ sub: '', exp: Date.now() / 1000 + 10 }),
|
|
260
278
|
isEncodingAsBson: false
|
|
261
279
|
});
|
|
262
|
-
|
|
263
|
-
|
|
280
|
+
const dataLines = [];
|
|
281
|
+
const partialCheckpoints = [];
|
|
282
|
+
const checkpoints = [];
|
|
283
|
+
const checkpointCompletes = [];
|
|
284
|
+
let afterCheckpointDiff = false;
|
|
285
|
+
let interruptionTriggered = false;
|
|
264
286
|
for await (let next of stream) {
|
|
265
287
|
if (typeof next == 'string') {
|
|
266
288
|
next = JSON.parse(next);
|
|
267
289
|
}
|
|
268
290
|
if (typeof next === 'object' && next !== null) {
|
|
269
291
|
if ('partial_checkpoint_complete' in next) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
292
|
+
partialCheckpoints.push({
|
|
293
|
+
priority: next.partial_checkpoint_complete.priority,
|
|
294
|
+
afterCheckpointDiff
|
|
295
|
+
});
|
|
296
|
+
if (next.partial_checkpoint_complete.priority == 2 && !interruptionTriggered) {
|
|
297
|
+
interruptionTriggered = true;
|
|
273
298
|
await writer.save({
|
|
274
299
|
sourceTable: testTable,
|
|
275
300
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -280,27 +305,69 @@ bucket_definitions:
|
|
|
280
305
|
afterReplicaId: 'highprio2'
|
|
281
306
|
});
|
|
282
307
|
await writer.commit('0/2');
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
// Low-priority sync from the first checkpoint was interrupted. This should not happen before
|
|
286
|
-
// 1000 low-priority items were synchronized.
|
|
287
|
-
expect(sentCheckpoints).toBe(2);
|
|
288
|
-
expect(sentRows).toBeGreaterThan(1000);
|
|
308
|
+
// Let the checkpoint watcher observe the update before requesting the next priority.
|
|
309
|
+
await timers.setTimeout(50);
|
|
289
310
|
}
|
|
290
311
|
}
|
|
291
312
|
if ('checkpoint' in next || 'checkpoint_diff' in next) {
|
|
292
|
-
|
|
313
|
+
checkpoints.push(next);
|
|
314
|
+
afterCheckpointDiff = 'checkpoint_diff' in next;
|
|
293
315
|
}
|
|
294
316
|
if ('data' in next) {
|
|
295
|
-
|
|
317
|
+
dataLines.push({
|
|
318
|
+
bucket: next.data.bucket,
|
|
319
|
+
objectIds: next.data.data.flatMap((entry) => (entry.object_id == null ? [] : [entry.object_id])),
|
|
320
|
+
afterCheckpointDiff
|
|
321
|
+
});
|
|
296
322
|
}
|
|
297
323
|
if ('checkpoint_complete' in next) {
|
|
324
|
+
checkpointCompletes.push(next);
|
|
298
325
|
break;
|
|
299
326
|
}
|
|
300
327
|
}
|
|
301
328
|
}
|
|
302
|
-
|
|
303
|
-
|
|
329
|
+
// Expected flow (data may be split into any number of chunks):
|
|
330
|
+
//
|
|
331
|
+
// checkpoint
|
|
332
|
+
// data: b1 contains highprio
|
|
333
|
+
// partial_checkpoint_complete: priority 1
|
|
334
|
+
// data: b0a contains all 999 first-* rows (1,000 operations including highprio)
|
|
335
|
+
// partial_checkpoint_complete: priority 2
|
|
336
|
+
// ## add highprio2, interrupting before b0b starts
|
|
337
|
+
// checkpoint_diff
|
|
338
|
+
// data: b1 contains highprio2
|
|
339
|
+
// partial_checkpoint_complete: priority 1
|
|
340
|
+
// data: b0b contains all 9,001 low-* rows carried over from the interrupted checkpoint
|
|
341
|
+
// checkpoint_complete: only for the new checkpoint
|
|
342
|
+
expect(interruptionTriggered).toBe(true);
|
|
343
|
+
expect(checkpoints).toHaveLength(2);
|
|
344
|
+
expect(checkpoints[0]).toHaveProperty('checkpoint');
|
|
345
|
+
expect(checkpoints[1]).toHaveProperty('checkpoint_diff');
|
|
346
|
+
expect(partialCheckpoints).toEqual([
|
|
347
|
+
{ priority: 1, afterCheckpointDiff: false },
|
|
348
|
+
{ priority: 2, afterCheckpointDiff: false },
|
|
349
|
+
{ priority: 1, afterCheckpointDiff: true }
|
|
350
|
+
]);
|
|
351
|
+
expect(checkpointCompletes).toEqual([
|
|
352
|
+
{
|
|
353
|
+
checkpoint_complete: {
|
|
354
|
+
last_op_id: checkpoints[1].checkpoint_diff.last_op_id
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
]);
|
|
358
|
+
const objectIdsFor = (bucket, afterDiff) => dataLines
|
|
359
|
+
.filter((line) => line.bucket == bucket && line.afterCheckpointDiff == afterDiff)
|
|
360
|
+
.flatMap((line) => line.objectIds);
|
|
361
|
+
expect(objectIdsFor(b1Bucket, false)).toEqual(['highprio']);
|
|
362
|
+
const b0aObjectIds = objectIdsFor(b0aBucket, false);
|
|
363
|
+
expect(new Set(b0aObjectIds)).toEqual(new Set(Array.from({ length: 999 }, (_, i) => `first-${i}`)));
|
|
364
|
+
expect(b0aObjectIds).toHaveLength(999);
|
|
365
|
+
expect(objectIdsFor(b0bBucket, false)).toEqual([]);
|
|
366
|
+
expect(objectIdsFor(b1Bucket, true)).toEqual(['highprio2']);
|
|
367
|
+
expect(objectIdsFor(b0aBucket, true)).toEqual([]);
|
|
368
|
+
const b0bObjectIds = objectIdsFor(b0bBucket, true);
|
|
369
|
+
expect(new Set(b0bObjectIds)).toEqual(new Set(Array.from({ length: 9_001 }, (_, i) => `low-${i}`)));
|
|
370
|
+
expect(b0bObjectIds).toHaveLength(9_001);
|
|
304
371
|
});
|
|
305
372
|
test('sync interruptions with unrelated data', async () => {
|
|
306
373
|
await using f = await factory();
|
|
@@ -420,33 +487,33 @@ bucket_definitions:
|
|
|
420
487
|
expect(sentCheckpoints).toBe(2);
|
|
421
488
|
expect(sentRows).toBe(10002);
|
|
422
489
|
});
|
|
423
|
-
test('
|
|
490
|
+
test('restarts updated low-priority buckets at a new checkpoint', async () => {
|
|
424
491
|
await using f = await factory();
|
|
425
|
-
// bucket0a -> send all data
|
|
426
|
-
// then interrupt checkpoint with new data for all buckets
|
|
427
|
-
// -> data for all buckets should be sent in the new checkpoint
|
|
428
492
|
const syncRules = await updateSyncRules(f, {
|
|
429
493
|
content: `
|
|
430
494
|
bucket_definitions:
|
|
431
495
|
b0a:
|
|
432
496
|
priority: 2
|
|
433
497
|
data:
|
|
434
|
-
- SELECT * FROM test WHERE
|
|
498
|
+
- SELECT * FROM test WHERE substring(id, 1, 6) = 'first-';
|
|
435
499
|
b0b:
|
|
436
|
-
priority:
|
|
500
|
+
priority: 3
|
|
437
501
|
data:
|
|
438
|
-
- SELECT * FROM test WHERE
|
|
502
|
+
- SELECT * FROM test WHERE substring(id, 1, 4) = 'low-';
|
|
439
503
|
b1:
|
|
440
504
|
priority: 1
|
|
441
505
|
data:
|
|
442
|
-
- SELECT * FROM test WHERE
|
|
506
|
+
- SELECT * FROM test WHERE substring(id, 1, 8) = 'highprio';
|
|
443
507
|
`
|
|
444
508
|
});
|
|
445
509
|
const bucketStorage = f.getInstance(syncRules);
|
|
446
510
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
447
511
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
512
|
+
const syncRulesContent = syncRules.syncConfigContent[0];
|
|
513
|
+
const b0aBucket = test_utils.bucketRequest(syncRulesContent, 'b0a[]').bucket;
|
|
514
|
+
const b0bBucket = test_utils.bucketRequest(syncRulesContent, 'b0b[]').bucket;
|
|
515
|
+
const b1Bucket = test_utils.bucketRequest(syncRulesContent, 'b1[]').bucket;
|
|
448
516
|
await writer.markAllSnapshotDone('0/1');
|
|
449
|
-
// Initial data: Add one priority row and 10k low-priority rows.
|
|
450
517
|
await writer.save({
|
|
451
518
|
sourceTable: testTable,
|
|
452
519
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -456,15 +523,26 @@ bucket_definitions:
|
|
|
456
523
|
},
|
|
457
524
|
afterReplicaId: 'highprio'
|
|
458
525
|
});
|
|
526
|
+
for (let i = 0; i < 999; i++) {
|
|
527
|
+
await writer.save({
|
|
528
|
+
sourceTable: testTable,
|
|
529
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
530
|
+
after: {
|
|
531
|
+
id: `first-${i}`,
|
|
532
|
+
description: 'first low-priority bucket'
|
|
533
|
+
},
|
|
534
|
+
afterReplicaId: `first-${i}`
|
|
535
|
+
});
|
|
536
|
+
}
|
|
459
537
|
for (let i = 0; i < 2_000; i++) {
|
|
460
538
|
await writer.save({
|
|
461
539
|
sourceTable: testTable,
|
|
462
540
|
tag: storage.SaveOperationTag.INSERT,
|
|
463
541
|
after: {
|
|
464
|
-
id:
|
|
542
|
+
id: `low-${i}`,
|
|
465
543
|
description: 'low prio'
|
|
466
544
|
},
|
|
467
|
-
afterReplicaId:
|
|
545
|
+
afterReplicaId: `low-${i}`
|
|
468
546
|
});
|
|
469
547
|
}
|
|
470
548
|
await writer.commit('0/1');
|
|
@@ -481,25 +559,24 @@ bucket_definitions:
|
|
|
481
559
|
token: new JwtPayload({ sub: '', exp: Date.now() / 1000 + 10 }),
|
|
482
560
|
isEncodingAsBson: false
|
|
483
561
|
});
|
|
484
|
-
|
|
485
|
-
|
|
562
|
+
const dataLines = [];
|
|
563
|
+
const partialCheckpoints = [];
|
|
564
|
+
const checkpoints = [];
|
|
565
|
+
const checkpointCompletes = [];
|
|
566
|
+
let afterCheckpointDiff = false;
|
|
567
|
+
let interruptionTriggered = false;
|
|
486
568
|
for await (let next of stream) {
|
|
487
569
|
if (typeof next == 'string') {
|
|
488
570
|
next = JSON.parse(next);
|
|
489
571
|
}
|
|
490
572
|
if (typeof next === 'object' && next !== null) {
|
|
491
573
|
if ('partial_checkpoint_complete' in next) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
lines.push({ data: { ...next.data, data: undefined } });
|
|
499
|
-
sentRows += next.data.data.length;
|
|
500
|
-
if (sentRows == 1001) {
|
|
501
|
-
// Save new data to interrupt the low-priority sync.
|
|
502
|
-
// Add another high-priority row. This should interrupt the long-running low-priority sync.
|
|
574
|
+
partialCheckpoints.push({
|
|
575
|
+
priority: next.partial_checkpoint_complete.priority,
|
|
576
|
+
afterCheckpointDiff
|
|
577
|
+
});
|
|
578
|
+
if (next.partial_checkpoint_complete.priority == 2 && !interruptionTriggered) {
|
|
579
|
+
interruptionTriggered = true;
|
|
503
580
|
await writer.save({
|
|
504
581
|
sourceTable: testTable,
|
|
505
582
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -509,48 +586,79 @@ bucket_definitions:
|
|
|
509
586
|
},
|
|
510
587
|
afterReplicaId: 'highprio2'
|
|
511
588
|
});
|
|
512
|
-
// Also add a low-priority row
|
|
513
589
|
await writer.save({
|
|
514
590
|
sourceTable: testTable,
|
|
515
591
|
tag: storage.SaveOperationTag.INSERT,
|
|
516
592
|
after: {
|
|
517
|
-
id: '
|
|
593
|
+
id: 'low-2000',
|
|
518
594
|
description: 'Another low-priority row'
|
|
519
595
|
},
|
|
520
|
-
afterReplicaId: '
|
|
596
|
+
afterReplicaId: 'low-2000'
|
|
521
597
|
});
|
|
522
598
|
await writer.commit('0/2');
|
|
523
|
-
|
|
524
|
-
if (sentRows >= 1000 && sentRows <= 2001) {
|
|
525
|
-
// pause for a bit to give the stream time to process interruptions.
|
|
526
|
-
// This covers the data batch above and the next one.
|
|
599
|
+
// Let the checkpoint watcher observe the update before requesting the next priority.
|
|
527
600
|
await timers.setTimeout(50);
|
|
528
601
|
}
|
|
529
602
|
}
|
|
603
|
+
if ('checkpoint' in next || 'checkpoint_diff' in next) {
|
|
604
|
+
checkpoints.push(next);
|
|
605
|
+
afterCheckpointDiff = 'checkpoint_diff' in next;
|
|
606
|
+
}
|
|
607
|
+
if ('data' in next) {
|
|
608
|
+
dataLines.push({
|
|
609
|
+
bucket: next.data.bucket,
|
|
610
|
+
objectIds: next.data.data.flatMap((entry) => (entry.object_id == null ? [] : [entry.object_id])),
|
|
611
|
+
afterCheckpointDiff
|
|
612
|
+
});
|
|
613
|
+
}
|
|
530
614
|
if ('checkpoint_complete' in next) {
|
|
531
|
-
|
|
615
|
+
checkpointCompletes.push(next);
|
|
532
616
|
break;
|
|
533
617
|
}
|
|
534
618
|
}
|
|
535
619
|
}
|
|
536
|
-
// Expected
|
|
620
|
+
// Expected flow (data may be split into any number of chunks):
|
|
537
621
|
//
|
|
538
|
-
// checkpoint
|
|
539
|
-
// data
|
|
540
|
-
// partial_checkpoint_complete
|
|
541
|
-
// data (
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
// checkpoint_diff
|
|
545
|
-
// data
|
|
546
|
-
// partial_checkpoint_complete
|
|
547
|
-
// data
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
expect(
|
|
553
|
-
expect(
|
|
622
|
+
// checkpoint
|
|
623
|
+
// data: b1 contains highprio
|
|
624
|
+
// partial_checkpoint_complete: priority 1
|
|
625
|
+
// data: b0a contains all 999 first-* rows (1,000 operations including highprio)
|
|
626
|
+
// partial_checkpoint_complete: priority 2
|
|
627
|
+
// ## add highprio2 and low-2000, interrupting before b0b starts
|
|
628
|
+
// checkpoint_diff
|
|
629
|
+
// data: b1 contains highprio2
|
|
630
|
+
// partial_checkpoint_complete: priority 1
|
|
631
|
+
// data: b0b contains all 2,001 low-* rows
|
|
632
|
+
// checkpoint_complete: only for the new checkpoint
|
|
633
|
+
expect(interruptionTriggered).toBe(true);
|
|
634
|
+
expect(checkpoints).toHaveLength(2);
|
|
635
|
+
expect(checkpoints[0]).toHaveProperty('checkpoint');
|
|
636
|
+
expect(checkpoints[1]).toHaveProperty('checkpoint_diff');
|
|
637
|
+
expect(partialCheckpoints).toEqual([
|
|
638
|
+
{ priority: 1, afterCheckpointDiff: false },
|
|
639
|
+
{ priority: 2, afterCheckpointDiff: false },
|
|
640
|
+
{ priority: 1, afterCheckpointDiff: true }
|
|
641
|
+
]);
|
|
642
|
+
expect(checkpointCompletes).toEqual([
|
|
643
|
+
{
|
|
644
|
+
checkpoint_complete: {
|
|
645
|
+
last_op_id: checkpoints[1].checkpoint_diff.last_op_id
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
]);
|
|
649
|
+
const objectIdsFor = (bucket, afterDiff) => dataLines
|
|
650
|
+
.filter((line) => line.bucket == bucket && line.afterCheckpointDiff == afterDiff)
|
|
651
|
+
.flatMap((line) => line.objectIds);
|
|
652
|
+
expect(objectIdsFor(b1Bucket, false)).toEqual(['highprio']);
|
|
653
|
+
const b0aObjectIds = objectIdsFor(b0aBucket, false);
|
|
654
|
+
expect(new Set(b0aObjectIds)).toEqual(new Set(Array.from({ length: 999 }, (_, i) => `first-${i}`)));
|
|
655
|
+
expect(b0aObjectIds).toHaveLength(999);
|
|
656
|
+
expect(objectIdsFor(b0bBucket, false)).toEqual([]);
|
|
657
|
+
expect(objectIdsFor(b1Bucket, true)).toEqual(['highprio2']);
|
|
658
|
+
expect(objectIdsFor(b0aBucket, true)).toEqual([]);
|
|
659
|
+
const b0bObjectIds = objectIdsFor(b0bBucket, true);
|
|
660
|
+
expect(new Set(b0bObjectIds)).toEqual(new Set(Array.from({ length: 2_001 }, (_, i) => `low-${i}`)));
|
|
661
|
+
expect(b0bObjectIds).toHaveLength(2_001);
|
|
554
662
|
});
|
|
555
663
|
test('sends checkpoint complete line for empty checkpoint', async () => {
|
|
556
664
|
await using f = await factory();
|
|
@@ -931,6 +1039,82 @@ bucket_definitions:
|
|
|
931
1039
|
const expLines = await getCheckpointLines(iter);
|
|
932
1040
|
expect(expLines).toMatchSnapshot();
|
|
933
1041
|
});
|
|
1042
|
+
test('checksum invalidation skips the candidate checkpoint', async (context) => {
|
|
1043
|
+
await using f = await factory();
|
|
1044
|
+
const syncRules = await updateSyncRules(f, {
|
|
1045
|
+
content: BASIC_SYNC_RULES
|
|
1046
|
+
});
|
|
1047
|
+
const bucketStorage = await f.getInstance(syncRules);
|
|
1048
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1049
|
+
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
1050
|
+
await writer.markAllSnapshotDone('0/1');
|
|
1051
|
+
await writer.save({
|
|
1052
|
+
sourceTable: testTable,
|
|
1053
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
1054
|
+
after: {
|
|
1055
|
+
id: 't1',
|
|
1056
|
+
description: 'Test 1'
|
|
1057
|
+
},
|
|
1058
|
+
afterReplicaId: 't1'
|
|
1059
|
+
});
|
|
1060
|
+
await writer.commit('0/1');
|
|
1061
|
+
let resolveInvalidatedChecksum;
|
|
1062
|
+
const invalidatedChecksum = new Promise((resolve) => {
|
|
1063
|
+
resolveInvalidatedChecksum = resolve;
|
|
1064
|
+
});
|
|
1065
|
+
const checksumSpy = vi.spyOn(bucketStorage, 'getChecksums').mockImplementationOnce(async (checkpoint, buckets) => {
|
|
1066
|
+
resolveInvalidatedChecksum();
|
|
1067
|
+
throw new storage.CheckpointChecksumInvalidatedError(checkpoint.checkpoint, buckets[0].bucket);
|
|
1068
|
+
});
|
|
1069
|
+
const stream = sync.streamResponse({
|
|
1070
|
+
syncContext,
|
|
1071
|
+
bucketStorage,
|
|
1072
|
+
syncRules: bucketStorage.getParsedSyncRules(test_utils.PARSE_OPTIONS),
|
|
1073
|
+
params: {
|
|
1074
|
+
buckets: [],
|
|
1075
|
+
include_checksum: true,
|
|
1076
|
+
raw_data: true
|
|
1077
|
+
},
|
|
1078
|
+
tracker,
|
|
1079
|
+
token: new JwtPayload({ sub: '', exp: Date.now() / 1000 + 10 }),
|
|
1080
|
+
isEncodingAsBson: false
|
|
1081
|
+
});
|
|
1082
|
+
const iter = stream[Symbol.asyncIterator]();
|
|
1083
|
+
context.onTestFinished(() => {
|
|
1084
|
+
iter.return?.();
|
|
1085
|
+
});
|
|
1086
|
+
const linesPromise = getCheckpointLines(iter, { consume: true });
|
|
1087
|
+
await invalidatedChecksum;
|
|
1088
|
+
// The first candidate was discarded before CheckpointLine.advance(). A later
|
|
1089
|
+
// checkpoint must therefore be calculated from the original connection state.
|
|
1090
|
+
await writer.save({
|
|
1091
|
+
sourceTable: testTable,
|
|
1092
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
1093
|
+
after: {
|
|
1094
|
+
id: 't2',
|
|
1095
|
+
description: 'Test 2'
|
|
1096
|
+
},
|
|
1097
|
+
afterReplicaId: 't2'
|
|
1098
|
+
});
|
|
1099
|
+
await writer.commit('0/2');
|
|
1100
|
+
const lines = await linesPromise;
|
|
1101
|
+
expect(checksumSpy).toHaveBeenCalledTimes(2);
|
|
1102
|
+
expect(lines[0]).toEqual({
|
|
1103
|
+
checkpoint: expect.objectContaining({
|
|
1104
|
+
last_op_id: '2'
|
|
1105
|
+
})
|
|
1106
|
+
});
|
|
1107
|
+
expect(lines).not.toContainEqual({
|
|
1108
|
+
checkpoint: expect.objectContaining({
|
|
1109
|
+
last_op_id: '1'
|
|
1110
|
+
})
|
|
1111
|
+
});
|
|
1112
|
+
expect(lines.at(-1)).toEqual({
|
|
1113
|
+
checkpoint_complete: expect.objectContaining({
|
|
1114
|
+
last_op_id: '2'
|
|
1115
|
+
})
|
|
1116
|
+
});
|
|
1117
|
+
});
|
|
934
1118
|
test('compacting data - invalidate checkpoint', async (context) => {
|
|
935
1119
|
// This tests a case of a compact operation invalidating a checkpoint in the
|
|
936
1120
|
// middle of syncing data.
|
|
@@ -1200,7 +1384,7 @@ bucket_definitions:
|
|
|
1200
1384
|
user_id: 'test',
|
|
1201
1385
|
heads: { '1': '1/0' }
|
|
1202
1386
|
}
|
|
1203
|
-
])).get('test');
|
|
1387
|
+
])).writeCheckpoints.get('test');
|
|
1204
1388
|
const params = {
|
|
1205
1389
|
syncContext,
|
|
1206
1390
|
bucketStorage,
|