@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/test-utils/AbstractStreamTestContext.d.ts +1 -1
  3. package/dist/test-utils/AbstractStreamTestContext.js +2 -3
  4. package/dist/test-utils/AbstractStreamTestContext.js.map +1 -1
  5. package/dist/test-utils/StorageDataHelpers.d.ts +3 -2
  6. package/dist/test-utils/StorageDataHelpers.js +40 -2
  7. package/dist/test-utils/StorageDataHelpers.js.map +1 -1
  8. package/dist/test-utils/general-utils.d.ts +3 -1
  9. package/dist/test-utils/general-utils.js +13 -1
  10. package/dist/test-utils/general-utils.js.map +1 -1
  11. package/dist/test-utils/storage-combinations.d.ts +13 -0
  12. package/dist/test-utils/storage-combinations.js +26 -0
  13. package/dist/test-utils/storage-combinations.js.map +1 -0
  14. package/dist/test-utils/stream_utils.d.ts +0 -1
  15. package/dist/test-utils/stream_utils.js +0 -10
  16. package/dist/test-utils/stream_utils.js.map +1 -1
  17. package/dist/test-utils/test-utils-index.d.ts +1 -0
  18. package/dist/test-utils/test-utils-index.js +1 -0
  19. package/dist/test-utils/test-utils-index.js.map +1 -1
  20. package/dist/tests/register-compacting-tests.js +15 -15
  21. package/dist/tests/register-compacting-tests.js.map +1 -1
  22. package/dist/tests/register-data-storage-checkpoint-tests.js +132 -14
  23. package/dist/tests/register-data-storage-checkpoint-tests.js.map +1 -1
  24. package/dist/tests/register-data-storage-data-tests.js +116 -96
  25. package/dist/tests/register-data-storage-data-tests.js.map +1 -1
  26. package/dist/tests/register-parameter-compacting-tests.js +3 -2
  27. package/dist/tests/register-parameter-compacting-tests.js.map +1 -1
  28. package/dist/tests/register-sync-tests.js +267 -82
  29. package/dist/tests/register-sync-tests.js.map +1 -1
  30. package/dist/tests/util.d.ts +6 -0
  31. package/dist/tests/util.js +12 -0
  32. package/dist/tests/util.js.map +1 -1
  33. package/package.json +4 -4
  34. package/src/test-utils/AbstractStreamTestContext.ts +2 -3
  35. package/src/test-utils/StorageDataHelpers.ts +45 -2
  36. package/src/test-utils/general-utils.ts +27 -3
  37. package/src/test-utils/storage-combinations.ts +51 -0
  38. package/src/test-utils/stream_utils.ts +0 -11
  39. package/src/test-utils/test-utils-index.ts +1 -0
  40. package/src/tests/register-compacting-tests.ts +15 -15
  41. package/src/tests/register-data-storage-checkpoint-tests.ts +161 -14
  42. package/src/tests/register-data-storage-data-tests.ts +132 -134
  43. package/src/tests/register-parameter-compacting-tests.ts +3 -2
  44. package/src/tests/register-sync-tests.ts +288 -84
  45. package/src/tests/util.ts +13 -0
  46. package/tsconfig.tsbuildinfo +1 -1
@@ -3,9 +3,10 @@ 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
+ import { compactActive } from './util.js';
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = path.dirname(__filename);
11
12
  const BASIC_SYNC_RULES = `
@@ -205,26 +206,33 @@ streams:
205
206
  const lines = await consumeCheckpointLines(stream);
206
207
  expect(lines).toMatchSnapshot();
207
208
  });
208
- test('sync interrupts low-priority buckets on new checkpoints', async () => {
209
+ test('carries pending low-priority buckets into a new checkpoint', async () => {
209
210
  await using f = await factory();
210
211
  const syncRules = await updateSyncRules(f, {
211
212
  content: `
212
213
  bucket_definitions:
213
- b0:
214
+ b0a:
214
215
  priority: 2
215
216
  data:
216
- - SELECT * FROM test WHERE LENGTH(id) <= 5;
217
+ - SELECT * FROM test WHERE substring(id, 1, 6) = 'first-';
218
+ b0b:
219
+ priority: 3
220
+ data:
221
+ - SELECT * FROM test WHERE substring(id, 1, 4) = 'low-';
217
222
  b1:
218
223
  priority: 1
219
224
  data:
220
- - SELECT * FROM test WHERE LENGTH(id) > 5;
225
+ - SELECT * FROM test WHERE substring(id, 1, 8) = 'highprio';
221
226
  `
222
227
  });
223
228
  const bucketStorage = f.getInstance(syncRules);
224
229
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
225
230
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
231
+ const syncRulesContent = syncRules.syncConfigContent[0];
232
+ const b0aBucket = test_utils.bucketRequest(syncRulesContent, 'b0a[]').bucket;
233
+ const b0bBucket = test_utils.bucketRequest(syncRulesContent, 'b0b[]').bucket;
234
+ const b1Bucket = test_utils.bucketRequest(syncRulesContent, 'b1[]').bucket;
226
235
  await writer.markAllSnapshotDone('0/1');
227
- // Initial data: Add one priority row and 10k low-priority rows.
228
236
  await writer.save({
229
237
  sourceTable: testTable,
230
238
  tag: storage.SaveOperationTag.INSERT,
@@ -234,15 +242,26 @@ bucket_definitions:
234
242
  },
235
243
  afterReplicaId: 'highprio'
236
244
  });
237
- for (let i = 0; i < 10_000; i++) {
245
+ for (let i = 0; i < 999; i++) {
238
246
  await writer.save({
239
247
  sourceTable: testTable,
240
248
  tag: storage.SaveOperationTag.INSERT,
241
249
  after: {
242
- id: `${i}`,
243
- description: 'low prio'
250
+ id: `first-${i}`,
251
+ description: 'first low-priority bucket'
244
252
  },
245
- afterReplicaId: `${i}`
253
+ afterReplicaId: `first-${i}`
254
+ });
255
+ }
256
+ for (let i = 0; i < 9_001; i++) {
257
+ await writer.save({
258
+ sourceTable: testTable,
259
+ tag: storage.SaveOperationTag.INSERT,
260
+ after: {
261
+ id: `low-${i}`,
262
+ description: 'last low-priority bucket'
263
+ },
264
+ afterReplicaId: `low-${i}`
246
265
  });
247
266
  }
248
267
  await writer.commit('0/1');
@@ -259,17 +278,24 @@ bucket_definitions:
259
278
  token: new JwtPayload({ sub: '', exp: Date.now() / 1000 + 10 }),
260
279
  isEncodingAsBson: false
261
280
  });
262
- let sentCheckpoints = 0;
263
- let sentRows = 0;
281
+ const dataLines = [];
282
+ const partialCheckpoints = [];
283
+ const checkpoints = [];
284
+ const checkpointCompletes = [];
285
+ let afterCheckpointDiff = false;
286
+ let interruptionTriggered = false;
264
287
  for await (let next of stream) {
265
288
  if (typeof next == 'string') {
266
289
  next = JSON.parse(next);
267
290
  }
268
291
  if (typeof next === 'object' && next !== null) {
269
292
  if ('partial_checkpoint_complete' in next) {
270
- if (sentCheckpoints == 1) {
271
- // Save new data to interrupt the low-priority sync.
272
- // Add another high-priority row. This should interrupt the long-running low-priority sync.
293
+ partialCheckpoints.push({
294
+ priority: next.partial_checkpoint_complete.priority,
295
+ afterCheckpointDiff
296
+ });
297
+ if (next.partial_checkpoint_complete.priority == 2 && !interruptionTriggered) {
298
+ interruptionTriggered = true;
273
299
  await writer.save({
274
300
  sourceTable: testTable,
275
301
  tag: storage.SaveOperationTag.INSERT,
@@ -280,27 +306,69 @@ bucket_definitions:
280
306
  afterReplicaId: 'highprio2'
281
307
  });
282
308
  await writer.commit('0/2');
283
- }
284
- else {
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);
309
+ // Let the checkpoint watcher observe the update before requesting the next priority.
310
+ await timers.setTimeout(50);
289
311
  }
290
312
  }
291
313
  if ('checkpoint' in next || 'checkpoint_diff' in next) {
292
- sentCheckpoints += 1;
314
+ checkpoints.push(next);
315
+ afterCheckpointDiff = 'checkpoint_diff' in next;
293
316
  }
294
317
  if ('data' in next) {
295
- sentRows += next.data.data.length;
318
+ dataLines.push({
319
+ bucket: next.data.bucket,
320
+ objectIds: next.data.data.flatMap((entry) => (entry.object_id == null ? [] : [entry.object_id])),
321
+ afterCheckpointDiff
322
+ });
296
323
  }
297
324
  if ('checkpoint_complete' in next) {
325
+ checkpointCompletes.push(next);
298
326
  break;
299
327
  }
300
328
  }
301
329
  }
302
- expect(sentCheckpoints).toBe(2);
303
- expect(sentRows).toBe(10002);
330
+ // Expected flow (data may be split into any number of chunks):
331
+ //
332
+ // checkpoint
333
+ // data: b1 contains highprio
334
+ // partial_checkpoint_complete: priority 1
335
+ // data: b0a contains all 999 first-* rows (1,000 operations including highprio)
336
+ // partial_checkpoint_complete: priority 2
337
+ // ## add highprio2, interrupting before b0b starts
338
+ // checkpoint_diff
339
+ // data: b1 contains highprio2
340
+ // partial_checkpoint_complete: priority 1
341
+ // data: b0b contains all 9,001 low-* rows carried over from the interrupted checkpoint
342
+ // checkpoint_complete: only for the new checkpoint
343
+ expect(interruptionTriggered).toBe(true);
344
+ expect(checkpoints).toHaveLength(2);
345
+ expect(checkpoints[0]).toHaveProperty('checkpoint');
346
+ expect(checkpoints[1]).toHaveProperty('checkpoint_diff');
347
+ expect(partialCheckpoints).toEqual([
348
+ { priority: 1, afterCheckpointDiff: false },
349
+ { priority: 2, afterCheckpointDiff: false },
350
+ { priority: 1, afterCheckpointDiff: true }
351
+ ]);
352
+ expect(checkpointCompletes).toEqual([
353
+ {
354
+ checkpoint_complete: {
355
+ last_op_id: checkpoints[1].checkpoint_diff.last_op_id
356
+ }
357
+ }
358
+ ]);
359
+ const objectIdsFor = (bucket, afterDiff) => dataLines
360
+ .filter((line) => line.bucket == bucket && line.afterCheckpointDiff == afterDiff)
361
+ .flatMap((line) => line.objectIds);
362
+ expect(objectIdsFor(b1Bucket, false)).toEqual(['highprio']);
363
+ const b0aObjectIds = objectIdsFor(b0aBucket, false);
364
+ expect(new Set(b0aObjectIds)).toEqual(new Set(Array.from({ length: 999 }, (_, i) => `first-${i}`)));
365
+ expect(b0aObjectIds).toHaveLength(999);
366
+ expect(objectIdsFor(b0bBucket, false)).toEqual([]);
367
+ expect(objectIdsFor(b1Bucket, true)).toEqual(['highprio2']);
368
+ expect(objectIdsFor(b0aBucket, true)).toEqual([]);
369
+ const b0bObjectIds = objectIdsFor(b0bBucket, true);
370
+ expect(new Set(b0bObjectIds)).toEqual(new Set(Array.from({ length: 9_001 }, (_, i) => `low-${i}`)));
371
+ expect(b0bObjectIds).toHaveLength(9_001);
304
372
  });
305
373
  test('sync interruptions with unrelated data', async () => {
306
374
  await using f = await factory();
@@ -420,33 +488,33 @@ bucket_definitions:
420
488
  expect(sentCheckpoints).toBe(2);
421
489
  expect(sentRows).toBe(10002);
422
490
  });
423
- test('sync interrupts low-priority buckets on new checkpoints (2)', async () => {
491
+ test('restarts updated low-priority buckets at a new checkpoint', async () => {
424
492
  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
493
  const syncRules = await updateSyncRules(f, {
429
494
  content: `
430
495
  bucket_definitions:
431
496
  b0a:
432
497
  priority: 2
433
498
  data:
434
- - SELECT * FROM test WHERE LENGTH(id) <= 5;
499
+ - SELECT * FROM test WHERE substring(id, 1, 6) = 'first-';
435
500
  b0b:
436
- priority: 2
501
+ priority: 3
437
502
  data:
438
- - SELECT * FROM test WHERE LENGTH(id) <= 5;
503
+ - SELECT * FROM test WHERE substring(id, 1, 4) = 'low-';
439
504
  b1:
440
505
  priority: 1
441
506
  data:
442
- - SELECT * FROM test WHERE LENGTH(id) > 5;
507
+ - SELECT * FROM test WHERE substring(id, 1, 8) = 'highprio';
443
508
  `
444
509
  });
445
510
  const bucketStorage = f.getInstance(syncRules);
446
511
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
447
512
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
513
+ const syncRulesContent = syncRules.syncConfigContent[0];
514
+ const b0aBucket = test_utils.bucketRequest(syncRulesContent, 'b0a[]').bucket;
515
+ const b0bBucket = test_utils.bucketRequest(syncRulesContent, 'b0b[]').bucket;
516
+ const b1Bucket = test_utils.bucketRequest(syncRulesContent, 'b1[]').bucket;
448
517
  await writer.markAllSnapshotDone('0/1');
449
- // Initial data: Add one priority row and 10k low-priority rows.
450
518
  await writer.save({
451
519
  sourceTable: testTable,
452
520
  tag: storage.SaveOperationTag.INSERT,
@@ -456,15 +524,26 @@ bucket_definitions:
456
524
  },
457
525
  afterReplicaId: 'highprio'
458
526
  });
527
+ for (let i = 0; i < 999; i++) {
528
+ await writer.save({
529
+ sourceTable: testTable,
530
+ tag: storage.SaveOperationTag.INSERT,
531
+ after: {
532
+ id: `first-${i}`,
533
+ description: 'first low-priority bucket'
534
+ },
535
+ afterReplicaId: `first-${i}`
536
+ });
537
+ }
459
538
  for (let i = 0; i < 2_000; i++) {
460
539
  await writer.save({
461
540
  sourceTable: testTable,
462
541
  tag: storage.SaveOperationTag.INSERT,
463
542
  after: {
464
- id: `${i}`,
543
+ id: `low-${i}`,
465
544
  description: 'low prio'
466
545
  },
467
- afterReplicaId: `${i}`
546
+ afterReplicaId: `low-${i}`
468
547
  });
469
548
  }
470
549
  await writer.commit('0/1');
@@ -481,25 +560,24 @@ bucket_definitions:
481
560
  token: new JwtPayload({ sub: '', exp: Date.now() / 1000 + 10 }),
482
561
  isEncodingAsBson: false
483
562
  });
484
- let sentRows = 0;
485
- let lines = [];
563
+ const dataLines = [];
564
+ const partialCheckpoints = [];
565
+ const checkpoints = [];
566
+ const checkpointCompletes = [];
567
+ let afterCheckpointDiff = false;
568
+ let interruptionTriggered = false;
486
569
  for await (let next of stream) {
487
570
  if (typeof next == 'string') {
488
571
  next = JSON.parse(next);
489
572
  }
490
573
  if (typeof next === 'object' && next !== null) {
491
574
  if ('partial_checkpoint_complete' in next) {
492
- lines.push(next);
493
- }
494
- if ('checkpoint' in next || 'checkpoint_diff' in next) {
495
- lines.push(next);
496
- }
497
- if ('data' in next) {
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.
575
+ partialCheckpoints.push({
576
+ priority: next.partial_checkpoint_complete.priority,
577
+ afterCheckpointDiff
578
+ });
579
+ if (next.partial_checkpoint_complete.priority == 2 && !interruptionTriggered) {
580
+ interruptionTriggered = true;
503
581
  await writer.save({
504
582
  sourceTable: testTable,
505
583
  tag: storage.SaveOperationTag.INSERT,
@@ -509,48 +587,79 @@ bucket_definitions:
509
587
  },
510
588
  afterReplicaId: 'highprio2'
511
589
  });
512
- // Also add a low-priority row
513
590
  await writer.save({
514
591
  sourceTable: testTable,
515
592
  tag: storage.SaveOperationTag.INSERT,
516
593
  after: {
517
- id: '2001',
594
+ id: 'low-2000',
518
595
  description: 'Another low-priority row'
519
596
  },
520
- afterReplicaId: '2001'
597
+ afterReplicaId: 'low-2000'
521
598
  });
522
599
  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.
600
+ // Let the checkpoint watcher observe the update before requesting the next priority.
527
601
  await timers.setTimeout(50);
528
602
  }
529
603
  }
604
+ if ('checkpoint' in next || 'checkpoint_diff' in next) {
605
+ checkpoints.push(next);
606
+ afterCheckpointDiff = 'checkpoint_diff' in next;
607
+ }
608
+ if ('data' in next) {
609
+ dataLines.push({
610
+ bucket: next.data.bucket,
611
+ objectIds: next.data.data.flatMap((entry) => (entry.object_id == null ? [] : [entry.object_id])),
612
+ afterCheckpointDiff
613
+ });
614
+ }
530
615
  if ('checkpoint_complete' in next) {
531
- lines.push(next);
616
+ checkpointCompletes.push(next);
532
617
  break;
533
618
  }
534
619
  }
535
620
  }
536
- // Expected lines (full details in snapshot):
621
+ // Expected flow (data may be split into any number of chunks):
537
622
  //
538
- // checkpoint (4001)
539
- // data (b1[] 0 -> 1)
540
- // partial_checkpoint_complete (4001, priority 1)
541
- // data (b0a[], 0 -> 2000)
542
- // ## adds new data, interrupting the checkpoint
543
- // data (b0a[], 2000 -> 4000) # expected - stream is already busy with this by the time it receives the interruption
544
- // checkpoint_diff (4004)
545
- // data (b1[], 1 -> 4002)
546
- // partial_checkpoint_complete (4004, priority 1)
547
- // data (b0a[], 4000 -> 4003)
548
- // data (b0b[], 0 -> 1999)
549
- // data (b0b[], 1999 -> 3999)
550
- // data (b0b[], 3999 -> 4004)
551
- // checkpoint_complete (4004)
552
- expect(lines).toMatchSnapshot();
553
- expect(sentRows).toBe(4004);
623
+ // checkpoint
624
+ // data: b1 contains highprio
625
+ // partial_checkpoint_complete: priority 1
626
+ // data: b0a contains all 999 first-* rows (1,000 operations including highprio)
627
+ // partial_checkpoint_complete: priority 2
628
+ // ## add highprio2 and low-2000, interrupting before b0b starts
629
+ // checkpoint_diff
630
+ // data: b1 contains highprio2
631
+ // partial_checkpoint_complete: priority 1
632
+ // data: b0b contains all 2,001 low-* rows
633
+ // checkpoint_complete: only for the new checkpoint
634
+ expect(interruptionTriggered).toBe(true);
635
+ expect(checkpoints).toHaveLength(2);
636
+ expect(checkpoints[0]).toHaveProperty('checkpoint');
637
+ expect(checkpoints[1]).toHaveProperty('checkpoint_diff');
638
+ expect(partialCheckpoints).toEqual([
639
+ { priority: 1, afterCheckpointDiff: false },
640
+ { priority: 2, afterCheckpointDiff: false },
641
+ { priority: 1, afterCheckpointDiff: true }
642
+ ]);
643
+ expect(checkpointCompletes).toEqual([
644
+ {
645
+ checkpoint_complete: {
646
+ last_op_id: checkpoints[1].checkpoint_diff.last_op_id
647
+ }
648
+ }
649
+ ]);
650
+ const objectIdsFor = (bucket, afterDiff) => dataLines
651
+ .filter((line) => line.bucket == bucket && line.afterCheckpointDiff == afterDiff)
652
+ .flatMap((line) => line.objectIds);
653
+ expect(objectIdsFor(b1Bucket, false)).toEqual(['highprio']);
654
+ const b0aObjectIds = objectIdsFor(b0aBucket, false);
655
+ expect(new Set(b0aObjectIds)).toEqual(new Set(Array.from({ length: 999 }, (_, i) => `first-${i}`)));
656
+ expect(b0aObjectIds).toHaveLength(999);
657
+ expect(objectIdsFor(b0bBucket, false)).toEqual([]);
658
+ expect(objectIdsFor(b1Bucket, true)).toEqual(['highprio2']);
659
+ expect(objectIdsFor(b0aBucket, true)).toEqual([]);
660
+ const b0bObjectIds = objectIdsFor(b0bBucket, true);
661
+ expect(new Set(b0bObjectIds)).toEqual(new Set(Array.from({ length: 2_001 }, (_, i) => `low-${i}`)));
662
+ expect(b0bObjectIds).toHaveLength(2_001);
554
663
  });
555
664
  test('sends checkpoint complete line for empty checkpoint', async () => {
556
665
  await using f = await factory();
@@ -931,6 +1040,82 @@ bucket_definitions:
931
1040
  const expLines = await getCheckpointLines(iter);
932
1041
  expect(expLines).toMatchSnapshot();
933
1042
  });
1043
+ test('checksum invalidation skips the candidate checkpoint', async (context) => {
1044
+ await using f = await factory();
1045
+ const syncRules = await updateSyncRules(f, {
1046
+ content: BASIC_SYNC_RULES
1047
+ });
1048
+ const bucketStorage = await f.getInstance(syncRules);
1049
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1050
+ const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
1051
+ await writer.markAllSnapshotDone('0/1');
1052
+ await writer.save({
1053
+ sourceTable: testTable,
1054
+ tag: storage.SaveOperationTag.INSERT,
1055
+ after: {
1056
+ id: 't1',
1057
+ description: 'Test 1'
1058
+ },
1059
+ afterReplicaId: 't1'
1060
+ });
1061
+ await writer.commit('0/1');
1062
+ let resolveInvalidatedChecksum;
1063
+ const invalidatedChecksum = new Promise((resolve) => {
1064
+ resolveInvalidatedChecksum = resolve;
1065
+ });
1066
+ const checksumSpy = vi.spyOn(bucketStorage, 'getChecksums').mockImplementationOnce(async (checkpoint, buckets) => {
1067
+ resolveInvalidatedChecksum();
1068
+ throw new storage.CheckpointChecksumInvalidatedError(checkpoint.checkpoint, buckets[0].bucket);
1069
+ });
1070
+ const stream = sync.streamResponse({
1071
+ syncContext,
1072
+ bucketStorage,
1073
+ syncRules: bucketStorage.getParsedSyncRules(test_utils.PARSE_OPTIONS),
1074
+ params: {
1075
+ buckets: [],
1076
+ include_checksum: true,
1077
+ raw_data: true
1078
+ },
1079
+ tracker,
1080
+ token: new JwtPayload({ sub: '', exp: Date.now() / 1000 + 10 }),
1081
+ isEncodingAsBson: false
1082
+ });
1083
+ const iter = stream[Symbol.asyncIterator]();
1084
+ context.onTestFinished(() => {
1085
+ iter.return?.();
1086
+ });
1087
+ const linesPromise = getCheckpointLines(iter, { consume: true });
1088
+ await invalidatedChecksum;
1089
+ // The first candidate was discarded before CheckpointLine.advance(). A later
1090
+ // checkpoint must therefore be calculated from the original connection state.
1091
+ await writer.save({
1092
+ sourceTable: testTable,
1093
+ tag: storage.SaveOperationTag.INSERT,
1094
+ after: {
1095
+ id: 't2',
1096
+ description: 'Test 2'
1097
+ },
1098
+ afterReplicaId: 't2'
1099
+ });
1100
+ await writer.commit('0/2');
1101
+ const lines = await linesPromise;
1102
+ expect(checksumSpy).toHaveBeenCalledTimes(2);
1103
+ expect(lines[0]).toEqual({
1104
+ checkpoint: expect.objectContaining({
1105
+ last_op_id: '2'
1106
+ })
1107
+ });
1108
+ expect(lines).not.toContainEqual({
1109
+ checkpoint: expect.objectContaining({
1110
+ last_op_id: '1'
1111
+ })
1112
+ });
1113
+ expect(lines.at(-1)).toEqual({
1114
+ checkpoint_complete: expect.objectContaining({
1115
+ last_op_id: '2'
1116
+ })
1117
+ });
1118
+ });
934
1119
  test('compacting data - invalidate checkpoint', async (context) => {
935
1120
  // This tests a case of a compact operation invalidating a checkpoint in the
936
1121
  // middle of syncing data.
@@ -943,6 +1128,7 @@ bucket_definitions:
943
1128
  const bucketStorage = await f.getInstance(syncRules);
944
1129
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
945
1130
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
1131
+ const bucket = bucketRequest(syncRules.syncConfigContent[0], 'mybucket[]').bucket;
946
1132
  await writer.markAllSnapshotDone('0/1');
947
1133
  await writer.save({
948
1134
  sourceTable: testTable,
@@ -1010,10 +1196,7 @@ bucket_definitions:
1010
1196
  afterReplicaId: 't2'
1011
1197
  });
1012
1198
  await writer.commit('0/2');
1013
- await bucketStorage.compact({
1014
- minBucketChanges: 1,
1015
- minChangeRatio: 0
1016
- });
1199
+ await compactActive(f, { compactBuckets: [bucket] });
1017
1200
  const lines2 = await getCheckpointLines(iter, { consume: true });
1018
1201
  // Snapshot test checks for changes in general.
1019
1202
  // The tests after that documents the specific things we're looking for
@@ -1084,6 +1267,7 @@ bucket_definitions:
1084
1267
  const bucketStorage = await f.getInstance(syncRules);
1085
1268
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1086
1269
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
1270
+ const highPriorityBucket = bucketRequest(syncRules.syncConfigContent[0], 'high_priority[]').bucket;
1087
1271
  await writer.markAllSnapshotDone('0/1');
1088
1272
  await writer.save({
1089
1273
  sourceTable: testTable,
@@ -1160,9 +1344,10 @@ bucket_definitions:
1160
1344
  afterReplicaId: 'high2'
1161
1345
  });
1162
1346
  await writer.commit('0/2');
1163
- await bucketStorage.compact({
1164
- minBucketChanges: 1,
1165
- minChangeRatio: 0
1347
+ await compactActive(f, {
1348
+ // Explicitly compact the high-priority bucket: V3 schedules background
1349
+ // compaction, while this test needs compaction at this exact point.
1350
+ compactBuckets: [highPriorityBucket]
1166
1351
  });
1167
1352
  const lines = await getCheckpointLines(iter, { consume: true });
1168
1353
  const nextCheckpointIndex = lines.findIndex((line) => line?.checkpoint_diff != null);
@@ -1200,7 +1385,7 @@ bucket_definitions:
1200
1385
  user_id: 'test',
1201
1386
  heads: { '1': '1/0' }
1202
1387
  }
1203
- ])).get('test');
1388
+ ])).writeCheckpoints.get('test');
1204
1389
  const params = {
1205
1390
  syncContext,
1206
1391
  bucketStorage,