@powersync/service-core-tests 0.0.0-dev-20260602073133 → 0.0.0-dev-20260909133214

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 (53) hide show
  1. package/CHANGELOG.md +112 -4
  2. package/dist/test-utils/AbstractStreamTestContext.d.ts +7 -6
  3. package/dist/test-utils/AbstractStreamTestContext.js +28 -25
  4. package/dist/test-utils/AbstractStreamTestContext.js.map +1 -1
  5. package/dist/test-utils/StorageDataHelpers.d.ts +6 -5
  6. package/dist/test-utils/StorageDataHelpers.js +40 -2
  7. package/dist/test-utils/StorageDataHelpers.js.map +1 -1
  8. package/dist/test-utils/bucket-validation.d.ts +3 -3
  9. package/dist/test-utils/bucket-validation.js +3 -3
  10. package/dist/test-utils/general-utils.d.ts +16 -3
  11. package/dist/test-utils/general-utils.js +37 -4
  12. package/dist/test-utils/general-utils.js.map +1 -1
  13. package/dist/test-utils/storage-combinations.d.ts +13 -0
  14. package/dist/test-utils/storage-combinations.js +26 -0
  15. package/dist/test-utils/storage-combinations.js.map +1 -0
  16. package/dist/test-utils/stream_utils.d.ts +0 -1
  17. package/dist/test-utils/stream_utils.js +0 -10
  18. package/dist/test-utils/stream_utils.js.map +1 -1
  19. package/dist/test-utils/test-utils-index.d.ts +1 -0
  20. package/dist/test-utils/test-utils-index.js +1 -0
  21. package/dist/test-utils/test-utils-index.js.map +1 -1
  22. package/dist/tests/register-compacting-tests.js +36 -29
  23. package/dist/tests/register-compacting-tests.js.map +1 -1
  24. package/dist/tests/register-data-storage-checkpoint-tests.js +138 -14
  25. package/dist/tests/register-data-storage-checkpoint-tests.js.map +1 -1
  26. package/dist/tests/register-data-storage-data-tests.js +205 -138
  27. package/dist/tests/register-data-storage-data-tests.js.map +1 -1
  28. package/dist/tests/register-data-storage-parameter-tests.js +44 -42
  29. package/dist/tests/register-data-storage-parameter-tests.js.map +1 -1
  30. package/dist/tests/register-parameter-compacting-tests.js +3 -2
  31. package/dist/tests/register-parameter-compacting-tests.js.map +1 -1
  32. package/dist/tests/register-sync-tests.js +404 -86
  33. package/dist/tests/register-sync-tests.js.map +1 -1
  34. package/dist/tests/util.d.ts +8 -2
  35. package/dist/tests/util.js +12 -0
  36. package/dist/tests/util.js.map +1 -1
  37. package/package.json +5 -6
  38. package/src/test-utils/AbstractStreamTestContext.ts +31 -27
  39. package/src/test-utils/StorageDataHelpers.ts +52 -8
  40. package/src/test-utils/bucket-validation.ts +3 -3
  41. package/src/test-utils/general-utils.ts +62 -10
  42. package/src/test-utils/storage-combinations.ts +51 -0
  43. package/src/test-utils/stream_utils.ts +0 -11
  44. package/src/test-utils/test-utils-index.ts +1 -0
  45. package/src/tests/register-compacting-tests.ts +55 -32
  46. package/src/tests/register-data-storage-checkpoint-tests.ts +171 -14
  47. package/src/tests/register-data-storage-data-tests.ts +251 -150
  48. package/src/tests/register-data-storage-parameter-tests.ts +58 -42
  49. package/src/tests/register-parameter-compacting-tests.ts +3 -2
  50. package/src/tests/register-sync-tests.ts +443 -88
  51. package/src/tests/util.ts +15 -2
  52. package/tsconfig.json +1 -4
  53. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,51 @@
1
+ import {
2
+ STORAGE_VERSION_1,
3
+ STORAGE_VERSION_2,
4
+ STORAGE_VERSION_4,
5
+ TestStorageConfig,
6
+ TestStorageFactory
7
+ } from '@powersync/service-core';
8
+ import { describe, TestOptions } from 'vitest';
9
+
10
+ const MONGO_STORAGE_VERSIONS = [STORAGE_VERSION_1, STORAGE_VERSION_2, STORAGE_VERSION_4];
11
+ const POSTGRES_STORAGE_VERSIONS = [STORAGE_VERSION_1, STORAGE_VERSION_2];
12
+
13
+ export interface StorageVersionTestContext {
14
+ factory: TestStorageFactory;
15
+ storageVersion: number;
16
+ }
17
+
18
+ export interface StorageCombinationTestConfig {
19
+ mongodb?: TestStorageConfig;
20
+ postgres?: TestStorageConfig;
21
+ }
22
+
23
+ export function describeStorageCombinations(
24
+ config: StorageCombinationTestConfig,
25
+ options: TestOptions & { storageVersions?: number[] },
26
+ fn: (context: StorageVersionTestContext) => void
27
+ ) {
28
+ const describeFactory = (storageName: string, storage: TestStorageConfig, supportedStorageVersions: number[]) => {
29
+ const storageVersions = (options.storageVersions ?? supportedStorageVersions).filter((version) =>
30
+ supportedStorageVersions.includes(version)
31
+ );
32
+ describe(`${storageName} storage`, options, function () {
33
+ for (const storageVersion of storageVersions) {
34
+ describe(`storage v${storageVersion}`, function () {
35
+ fn({
36
+ factory: storage.factory,
37
+ storageVersion
38
+ });
39
+ });
40
+ }
41
+ });
42
+ };
43
+
44
+ if (config.mongodb) {
45
+ describeFactory('mongodb', config.mongodb, MONGO_STORAGE_VERSIONS);
46
+ }
47
+
48
+ if (config.postgres) {
49
+ describeFactory('postgres', config.postgres, POSTGRES_STORAGE_VERSIONS);
50
+ }
51
+ }
@@ -22,17 +22,6 @@ export function compareIds(a: utils.OplogEntry, b: utils.OplogEntry) {
22
22
  return a.object_id!.localeCompare(b.object_id!);
23
23
  }
24
24
 
25
- export async function oneFromAsync<T>(source: Iterable<T> | AsyncIterable<T>): Promise<T> {
26
- const items: T[] = [];
27
- for await (const item of source) {
28
- items.push(item);
29
- }
30
- if (items.length != 1) {
31
- throw new Error(`One item expected, got: ${items.length}`);
32
- }
33
- return items[0];
34
- }
35
-
36
25
  export async function fromAsync<T>(source: Iterable<T> | AsyncIterable<T>): Promise<T[]> {
37
26
  const items: T[] = [];
38
27
  for await (const item of source) {
@@ -2,5 +2,6 @@ export * from './AbstractStreamTestContext.js';
2
2
  export * from './bucket-validation.js';
3
3
  export * from './general-utils.js';
4
4
  export * from './MetricsHelper.js';
5
+ export * from './storage-combinations.js';
5
6
  export * from './StorageDataHelpers.js';
6
7
  export * from './stream_utils.js';
@@ -2,7 +2,7 @@ import { addChecksums, storage, updateSyncRulesFromYaml } from '@powersync/servi
2
2
  import { expect, test } from 'vitest';
3
3
  import * as test_utils from '../test-utils/test-utils-index.js';
4
4
  import { bucketRequest } from '../test-utils/test-utils-index.js';
5
- import { bucketRequestMap, bucketRequests } from './util.js';
5
+ import { bucketRequestMap, bucketRequests, compactActive } from './util.js';
6
6
 
7
7
  export function registerCompactTests(config: storage.TestStorageConfig) {
8
8
  const generateStorageFactory = config.factory;
@@ -17,6 +17,7 @@ bucket_definitions:
17
17
  `)
18
18
  );
19
19
  const bucketStorage = factory.getInstance(syncRules);
20
+ const syncRulesContent = syncRules.syncConfigContent[0];
20
21
 
21
22
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
22
23
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
@@ -53,11 +54,13 @@ bucket_definitions:
53
54
 
54
55
  const checkpoint = writer.last_flushed_op!;
55
56
 
56
- const request = bucketRequest(syncRules, 'global[]');
57
+ const request = bucketRequest(syncRulesContent, 'global[]');
57
58
 
58
- const batchBefore = await test_utils.oneFromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
59
+ const batchBefore = await test_utils.getSingleBatchItem(
60
+ bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(checkpoint), [request])
61
+ );
59
62
  const dataBefore = batchBefore.chunkData.data;
60
- const checksumBefore = await bucketStorage.getChecksums(checkpoint, [request]);
63
+ const checksumBefore = await bucketStorage.getChecksums(test_utils.testCheckpoint(checkpoint), [request]);
61
64
 
62
65
  expect(dataBefore).toMatchObject([
63
66
  {
@@ -78,7 +81,7 @@ bucket_definitions:
78
81
  ]);
79
82
  expect(batchBefore.targetOp).toEqual(null);
80
83
 
81
- await bucketStorage.compact({
84
+ await compactActive(factory, {
82
85
  clearBatchLimit: 2,
83
86
  moveBatchLimit: 1,
84
87
  moveBatchQueryLimit: 1,
@@ -86,11 +89,13 @@ bucket_definitions:
86
89
  minChangeRatio: 0
87
90
  });
88
91
 
89
- const batchAfter = await test_utils.oneFromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
92
+ const batchAfter = await test_utils.getSingleBatchItem(
93
+ bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(checkpoint), [request])
94
+ );
90
95
  const dataAfter = batchAfter.chunkData.data;
91
- const checksumAfter = await bucketStorage.getChecksums(checkpoint, [request]);
96
+ const checksumAfter = await bucketStorage.getChecksums(test_utils.testCheckpoint(checkpoint), [request]);
92
97
  bucketStorage.clearChecksumCache();
93
- const checksumAfter2 = await bucketStorage.getChecksums(checkpoint, [request]);
98
+ const checksumAfter2 = await bucketStorage.getChecksums(test_utils.testCheckpoint(checkpoint), [request]);
94
99
 
95
100
  expect(batchAfter.targetOp).toEqual(3n);
96
101
  expect(dataAfter).toMatchObject([
@@ -124,6 +129,7 @@ bucket_definitions:
124
129
  `)
125
130
  );
126
131
  const bucketStorage = factory.getInstance(syncRules);
132
+ const syncRulesContent = syncRules.syncConfigContent[0];
127
133
 
128
134
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
129
135
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
@@ -168,11 +174,13 @@ bucket_definitions:
168
174
  await writer.flush();
169
175
 
170
176
  const checkpoint = writer.last_flushed_op!;
171
- const request = bucketRequest(syncRules, 'global[]');
177
+ const request = bucketRequest(syncRulesContent, 'global[]');
172
178
 
173
- const batchBefore = await test_utils.oneFromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
179
+ const batchBefore = await test_utils.getSingleBatchItem(
180
+ bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(checkpoint), [request])
181
+ );
174
182
  const dataBefore = batchBefore.chunkData.data;
175
- const checksumBefore = await bucketStorage.getChecksums(checkpoint, [request]);
183
+ const checksumBefore = await bucketStorage.getChecksums(test_utils.testCheckpoint(checkpoint), [request]);
176
184
 
177
185
  // op_id sequence depends on the storage implementation
178
186
  expect(dataBefore).toMatchObject([
@@ -194,7 +202,7 @@ bucket_definitions:
194
202
  }
195
203
  ]);
196
204
 
197
- await bucketStorage.compact({
205
+ await compactActive(factory, {
198
206
  clearBatchLimit: 2,
199
207
  moveBatchLimit: 1,
200
208
  moveBatchQueryLimit: 1,
@@ -202,10 +210,12 @@ bucket_definitions:
202
210
  minChangeRatio: 0
203
211
  });
204
212
 
205
- const batchAfter = await test_utils.oneFromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
213
+ const batchAfter = await test_utils.getSingleBatchItem(
214
+ bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(checkpoint), [request])
215
+ );
206
216
  const dataAfter = batchAfter.chunkData.data;
207
217
  bucketStorage.clearChecksumCache();
208
- const checksumAfter = await bucketStorage.getChecksums(checkpoint, [request]);
218
+ const checksumAfter = await bucketStorage.getChecksums(test_utils.testCheckpoint(checkpoint), [request]);
209
219
 
210
220
  expect(batchAfter.targetOp).toBeLessThanOrEqual(checkpoint);
211
221
  expect(dataAfter).toMatchObject([
@@ -240,6 +250,7 @@ bucket_definitions:
240
250
  `)
241
251
  );
242
252
  const bucketStorage = factory.getInstance(syncRules);
253
+ const syncRulesContent = syncRules.syncConfigContent[0];
243
254
 
244
255
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
245
256
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
@@ -275,7 +286,7 @@ bucket_definitions:
275
286
  await writer.flush();
276
287
 
277
288
  const checkpoint1 = writer.last_flushed_op!;
278
- const request = bucketRequest(syncRules, 'global[]');
289
+ const request = bucketRequest(syncRulesContent, 'global[]');
279
290
  await using writer2 = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
280
291
  const testTable2 = await test_utils.resolveTestTable(writer2, 'test', ['id'], config);
281
292
  await writer2.save({
@@ -290,7 +301,7 @@ bucket_definitions:
290
301
  await writer2.flush();
291
302
  const checkpoint2 = writer2.last_flushed_op!;
292
303
 
293
- await bucketStorage.compact({
304
+ await compactActive(factory, {
294
305
  clearBatchLimit: 2,
295
306
  moveBatchLimit: 1,
296
307
  moveBatchQueryLimit: 1,
@@ -298,10 +309,12 @@ bucket_definitions:
298
309
  minChangeRatio: 0
299
310
  });
300
311
 
301
- const batchAfter = await test_utils.oneFromAsync(bucketStorage.getBucketDataBatch(checkpoint2, [request]));
312
+ const batchAfter = await test_utils.getSingleBatchItem(
313
+ bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(checkpoint2), [request])
314
+ );
302
315
  const dataAfter = batchAfter.chunkData.data;
303
316
  await bucketStorage.clearChecksumCache();
304
- const checksumAfter = await bucketStorage.getChecksums(checkpoint2, [request]);
317
+ const checksumAfter = await bucketStorage.getChecksums(test_utils.testCheckpoint(checkpoint2), [request]);
305
318
 
306
319
  expect(dataAfter).toMatchObject([
307
320
  {
@@ -328,6 +341,7 @@ bucket_definitions:
328
341
  - select * from test where b = bucket.b`)
329
342
  );
330
343
  const bucketStorage = factory.getInstance(syncRules);
344
+ const syncRulesContent = syncRules.syncConfigContent[0];
331
345
 
332
346
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
333
347
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
@@ -402,7 +416,7 @@ bucket_definitions:
402
416
 
403
417
  const checkpoint = writer.last_flushed_op!;
404
418
 
405
- await bucketStorage.compact({
419
+ await compactActive(factory, {
406
420
  clearBatchLimit: 100,
407
421
  moveBatchLimit: 100,
408
422
  moveBatchQueryLimit: 100, // Larger limit for a larger window of operations
@@ -410,10 +424,10 @@ bucket_definitions:
410
424
  minChangeRatio: 0
411
425
  });
412
426
 
413
- const batchAfter = await test_utils.fromAsync(
427
+ const batchAfter = await test_utils.getBatchArray(
414
428
  bucketStorage.getBucketDataBatch(
415
- checkpoint,
416
- bucketRequestMap(syncRules, [
429
+ test_utils.testCheckpoint(checkpoint),
430
+ bucketRequestMap(syncRulesContent, [
417
431
  ['grouped["b1"]', 0n],
418
432
  ['grouped["b2"]', 0n]
419
433
  ])
@@ -458,6 +472,7 @@ bucket_definitions:
458
472
  `)
459
473
  );
460
474
  const bucketStorage = factory.getInstance(syncRules);
475
+ const syncRulesContent = syncRules.syncConfigContent[0];
461
476
 
462
477
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
463
478
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
@@ -492,7 +507,7 @@ bucket_definitions:
492
507
  await writer.commit('1/1');
493
508
  await writer.flush();
494
509
 
495
- await bucketStorage.compact({
510
+ await compactActive(factory, {
496
511
  clearBatchLimit: 2,
497
512
  moveBatchLimit: 1,
498
513
  moveBatchQueryLimit: 1,
@@ -513,9 +528,9 @@ bucket_definitions:
513
528
  await writer2.commit('2/1');
514
529
  await writer2.flush();
515
530
  const checkpoint2 = writer2.last_flushed_op!;
516
- const request = bucketRequest(syncRules, 'global[]');
531
+ const request = bucketRequest(syncRulesContent, 'global[]');
517
532
  await bucketStorage.clearChecksumCache();
518
- const checksumAfter = await bucketStorage.getChecksums(checkpoint2, [request]);
533
+ const checksumAfter = await bucketStorage.getChecksums(test_utils.testCheckpoint(checkpoint2), [request]);
519
534
  const globalChecksum = checksumAfter.get(request.bucket);
520
535
  expect(globalChecksum).toMatchObject({
521
536
  bucket: request.bucket,
@@ -536,6 +551,7 @@ bucket_definitions:
536
551
  `)
537
552
  );
538
553
  const bucketStorage = factory.getInstance(syncRules);
554
+ const syncRulesContent = syncRules.syncConfigContent[0];
539
555
 
540
556
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
541
557
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
@@ -562,7 +578,10 @@ bucket_definitions:
562
578
  await writer.flush();
563
579
 
564
580
  // Get checksums here just to populate the cache
565
- await bucketStorage.getChecksums(writer.last_flushed_op!, bucketRequests(syncRules, ['global[]']));
581
+ await bucketStorage.getChecksums(
582
+ test_utils.testCheckpoint(writer.last_flushed_op!),
583
+ bucketRequests(syncRulesContent, ['global[]'])
584
+ );
566
585
  await using writer2 = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
567
586
  const testTable2 = await test_utils.resolveTestTable(writer2, 'test', ['id'], config);
568
587
  await writer2.save({
@@ -576,7 +595,7 @@ bucket_definitions:
576
595
  await writer2.commit('2/1');
577
596
  await writer2.flush();
578
597
 
579
- await bucketStorage.compact({
598
+ await compactActive(factory, {
580
599
  clearBatchLimit: 20,
581
600
  moveBatchLimit: 10,
582
601
  moveBatchQueryLimit: 10,
@@ -585,9 +604,9 @@ bucket_definitions:
585
604
  });
586
605
 
587
606
  const checkpoint2 = writer2.last_flushed_op!;
588
- const request = bucketRequest(syncRules, 'global[]');
607
+ const request = bucketRequest(syncRulesContent, 'global[]');
589
608
  // Check that the checksum was correctly updated with the clear operation after having a cached checksum
590
- const checksumAfter = await bucketStorage.getChecksums(checkpoint2, [request]);
609
+ const checksumAfter = await bucketStorage.getChecksums(test_utils.testCheckpoint(checkpoint2), [request]);
591
610
  const globalChecksum = checksumAfter.get(request.bucket);
592
611
  expect(globalChecksum).toMatchObject({
593
612
  bucket: request.bucket,
@@ -607,6 +626,7 @@ bucket_definitions:
607
626
  `)
608
627
  );
609
628
  const bucketStorage = factory.getInstance(syncRules);
629
+ const syncRulesContent = syncRules.syncConfigContent[0];
610
630
 
611
631
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
612
632
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
@@ -638,15 +658,18 @@ bucket_definitions:
638
658
  expect(checkpointBeforeCompact.checkpoint).toEqual(checkpoint1);
639
659
 
640
660
  // With default options, Postgres compaction should use the active checkpoint.
641
- await bucketStorage.compact({
661
+ await compactActive(factory, {
642
662
  moveBatchLimit: 1,
643
663
  moveBatchQueryLimit: 1,
644
664
  minBucketChanges: 1,
645
665
  minChangeRatio: 0
646
666
  });
647
667
 
648
- const batchAfterDefaultCompact = await test_utils.oneFromAsync(
649
- bucketStorage.getBucketDataBatch(checkpoint2, bucketRequestMap(syncRules, [['global[]', 0n]]))
668
+ const batchAfterDefaultCompact = await test_utils.getSingleBatchItem(
669
+ bucketStorage.getBucketDataBatch(
670
+ test_utils.testCheckpoint(checkpoint2),
671
+ bucketRequestMap(syncRulesContent, [['global[]', 0n]])
672
+ )
650
673
  );
651
674
 
652
675
  // Operation 1 should remain a PUT because op_id=2 is above the default maxOpId checkpoint.
@@ -1,6 +1,7 @@
1
1
  import { storage, updateSyncRulesFromYaml } from '@powersync/service-core';
2
2
  import { expect, test } from 'vitest';
3
3
  import * as test_utils from '../test-utils/test-utils-index.js';
4
+ import { compactActive } from './util.js';
4
5
 
5
6
  /**
6
7
  * @example
@@ -36,13 +37,16 @@ bucket_definitions:
36
37
  const abortController = new AbortController();
37
38
  context.onTestFinished(() => abortController.abort());
38
39
  const iter = bucketStorage
39
- .watchCheckpointChanges({ user_id: 'user1', signal: abortController.signal })
40
+ .watchCheckpointChanges({
41
+ user_id: 'user1',
42
+ signal: abortController.signal
43
+ })
40
44
  [Symbol.asyncIterator]();
41
45
 
42
46
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
43
47
  await writer.markAllSnapshotDone('1/1');
44
48
 
45
- const writeCheckpoint = await bucketStorage.createManagedWriteCheckpoint({
49
+ const writeCheckpoint = await createManagedWriteCheckpoint(bucketStorage, {
46
50
  heads: { '1': '5/0' },
47
51
  user_id: 'user1'
48
52
  });
@@ -85,7 +89,10 @@ bucket_definitions:
85
89
  const abortController = new AbortController();
86
90
  context.onTestFinished(() => abortController.abort());
87
91
  const iter = bucketStorage
88
- .watchCheckpointChanges({ user_id: 'user1', signal: abortController.signal })
92
+ .watchCheckpointChanges({
93
+ user_id: 'user1',
94
+ signal: abortController.signal
95
+ })
89
96
  [Symbol.asyncIterator]();
90
97
 
91
98
  await writer.keepalive('5/0');
@@ -102,12 +109,12 @@ bucket_definitions:
102
109
  }
103
110
  });
104
111
 
105
- const writeCheckpoint = await bucketStorage.createManagedWriteCheckpoint({
112
+ const writeCheckpoint = await createManagedWriteCheckpoint(bucketStorage, {
106
113
  heads: { '1': '6/0' },
107
114
  user_id: 'user1'
108
115
  });
109
116
  // We have to trigger a new keepalive after the checkpoint, at least to cover postgres storage.
110
- // This is what is effetively triggered with RouteAPI.createReplicationHead().
117
+ // This is what is effectively triggered by RouteAPI.createReplicationHead().
111
118
  // MongoDB storage doesn't explicitly need this anymore.
112
119
  await writer.keepalive('6/0');
113
120
 
@@ -128,7 +135,7 @@ bucket_definitions:
128
135
  });
129
136
  });
130
137
 
131
- test('custom write checkpoints - checkpoint after write', async (context) => {
138
+ test('managed write checkpoints - client supplied ids are monotonic', async () => {
132
139
  await using factory = await generateStorageFactory();
133
140
  const r = await factory.configureSyncRules(
134
141
  updateSyncRulesFromYaml(
@@ -144,7 +151,75 @@ bucket_definitions:
144
151
  )
145
152
  );
146
153
  const bucketStorage = factory.getInstance(r.persisted_sync_rules!);
147
- bucketStorage.setWriteCheckpointMode(storage.WriteCheckpointMode.CUSTOM);
154
+
155
+ const first = await createManagedWriteCheckpointResult(bucketStorage, {
156
+ user_id: 'user1',
157
+ heads: { '1': '5/0' },
158
+ checkpoint_request_id: 42n
159
+ });
160
+ expect(first.writeCheckpoints.get('user1')).toEqual(42n);
161
+ expect(first.shouldAdvance).toBe(true);
162
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'user1', heads: { '1': '5/0' } })).resolves.toEqual(42n);
163
+
164
+ const stale = await createManagedWriteCheckpointResult(bucketStorage, {
165
+ user_id: 'user1',
166
+ heads: { '1': '8/0' },
167
+ checkpoint_request_id: 41n
168
+ });
169
+ expect(stale.writeCheckpoints.get('user1')).toEqual(42n);
170
+ // The stored checkpoint is unchanged but has not been processed by replication
171
+ // yet, so the source marker must still be forced (e.g. for a retry after a
172
+ // previous attempt failed to advance the marker).
173
+ expect(stale.shouldAdvance).toBe(true);
174
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'user1', heads: { '1': '6/0' } })).resolves.toEqual(42n);
175
+
176
+ const retried = await createManagedWriteCheckpointResult(bucketStorage, {
177
+ user_id: 'user1',
178
+ heads: { '1': '8/0' },
179
+ checkpoint_request_id: 42n
180
+ });
181
+ expect(retried.writeCheckpoints.get('user1')).toEqual(42n);
182
+ // Same as above: the duplicate request still points at a pending checkpoint.
183
+ expect(retried.shouldAdvance).toBe(true);
184
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'user1', heads: { '1': '6/0' } })).resolves.toEqual(42n);
185
+
186
+ const advanced = await createManagedWriteCheckpointResult(bucketStorage, {
187
+ user_id: 'user1',
188
+ heads: { '1': '8/0' },
189
+ checkpoint_request_id: 43n
190
+ });
191
+ expect(advanced.writeCheckpoints.get('user1')).toEqual(43n);
192
+ expect(advanced.shouldAdvance).toBe(true);
193
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'user1', heads: { '1': '7/0' } })).resolves.toBeNull();
194
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'user1', heads: { '1': '8/0' } })).resolves.toEqual(43n);
195
+
196
+ const generated = await createManagedWriteCheckpointResult(bucketStorage, {
197
+ user_id: 'user1',
198
+ heads: { '1': '9/0' }
199
+ });
200
+ expect(generated.writeCheckpoints.get('user1')).toEqual(44n);
201
+ expect(generated.shouldAdvance).toBe(true);
202
+ });
203
+
204
+ test.runIf((storageVersion ?? 1) < 3)('custom write checkpoints - checkpoint after write', async (context) => {
205
+ await using factory = await generateStorageFactory();
206
+ const r = await factory.configureSyncRules(
207
+ updateSyncRulesFromYaml(
208
+ `
209
+ bucket_definitions:
210
+ mybucket:
211
+ data: []
212
+ `,
213
+ {
214
+ validate: false,
215
+ storageVersion
216
+ }
217
+ )
218
+ );
219
+ const bucketStorage = factory.getInstance(r.persisted_sync_rules!);
220
+ bucketStorage.setWriteCheckpointMode({
221
+ mode: storage.WriteCheckpointMode.CUSTOM
222
+ });
148
223
 
149
224
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
150
225
  await writer.markAllSnapshotDone('1/1');
@@ -152,7 +227,10 @@ bucket_definitions:
152
227
  const abortController = new AbortController();
153
228
  context.onTestFinished(() => abortController.abort());
154
229
  const iter = bucketStorage
155
- .watchCheckpointChanges({ user_id: 'user1', signal: abortController.signal })
230
+ .watchCheckpointChanges({
231
+ user_id: 'user1',
232
+ signal: abortController.signal
233
+ })
156
234
  [Symbol.asyncIterator]();
157
235
 
158
236
  writer.addCustomWriteCheckpoint({
@@ -174,7 +252,7 @@ bucket_definitions:
174
252
  });
175
253
  });
176
254
 
177
- test('custom write checkpoints - standalone checkpoint', async (context) => {
255
+ test.runIf((storageVersion ?? 1) < 3)('custom write checkpoints - standalone checkpoint', async (context) => {
178
256
  await using factory = await generateStorageFactory();
179
257
  const r = await factory.configureSyncRules(
180
258
  updateSyncRulesFromYaml(
@@ -190,7 +268,9 @@ bucket_definitions:
190
268
  )
191
269
  );
192
270
  const bucketStorage = factory.getInstance(r.persisted_sync_rules!);
193
- bucketStorage.setWriteCheckpointMode(storage.WriteCheckpointMode.CUSTOM);
271
+ bucketStorage.setWriteCheckpointMode({
272
+ mode: storage.WriteCheckpointMode.CUSTOM
273
+ });
194
274
 
195
275
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
196
276
  await writer.markAllSnapshotDone('1/1');
@@ -198,7 +278,10 @@ bucket_definitions:
198
278
  const abortController = new AbortController();
199
279
  context.onTestFinished(() => abortController.abort());
200
280
  const iter = bucketStorage
201
- .watchCheckpointChanges({ user_id: 'user1', signal: abortController.signal })
281
+ .watchCheckpointChanges({
282
+ user_id: 'user1',
283
+ signal: abortController.signal
284
+ })
202
285
  [Symbol.asyncIterator]();
203
286
 
204
287
  // Flush to clear state
@@ -223,7 +306,7 @@ bucket_definitions:
223
306
  });
224
307
  });
225
308
 
226
- test('custom write checkpoints - write after checkpoint', async (context) => {
309
+ test.runIf((storageVersion ?? 1) < 3)('custom write checkpoints - write after checkpoint', async (context) => {
227
310
  await using factory = await generateStorageFactory();
228
311
  const r = await factory.configureSyncRules(
229
312
  updateSyncRulesFromYaml(
@@ -239,7 +322,9 @@ bucket_definitions:
239
322
  )
240
323
  );
241
324
  const bucketStorage = factory.getInstance(r.persisted_sync_rules!);
242
- bucketStorage.setWriteCheckpointMode(storage.WriteCheckpointMode.CUSTOM);
325
+ bucketStorage.setWriteCheckpointMode({
326
+ mode: storage.WriteCheckpointMode.CUSTOM
327
+ });
243
328
 
244
329
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
245
330
  await writer.markAllSnapshotDone('1/1');
@@ -247,7 +332,10 @@ bucket_definitions:
247
332
  const abortController = new AbortController();
248
333
  context.onTestFinished(() => abortController.abort());
249
334
  const iter = bucketStorage
250
- .watchCheckpointChanges({ user_id: 'user1', signal: abortController.signal })
335
+ .watchCheckpointChanges({
336
+ user_id: 'user1',
337
+ signal: abortController.signal
338
+ })
251
339
  [Symbol.asyncIterator]();
252
340
 
253
341
  await writer.keepalive('5/0');
@@ -301,4 +389,73 @@ bucket_definitions:
301
389
  }
302
390
  });
303
391
  });
392
+
393
+ test.runIf((storageVersion ?? 1) < 3)(
394
+ 'custom write checkpoints - checkpoint request markers are temporary',
395
+ async () => {
396
+ await using factory = await generateStorageFactory();
397
+ const r = await factory.configureSyncRules(
398
+ updateSyncRulesFromYaml(
399
+ `
400
+ bucket_definitions:
401
+ mybucket:
402
+ data: []
403
+ `,
404
+ {
405
+ validate: false,
406
+ storageVersion
407
+ }
408
+ )
409
+ );
410
+ const bucketStorage = factory.getInstance(r.persisted_sync_rules!);
411
+ bucketStorage.setWriteCheckpointMode({
412
+ mode: storage.WriteCheckpointMode.CUSTOM
413
+ });
414
+
415
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
416
+ await writer.markAllSnapshotDone('1/1');
417
+
418
+ writer.addCustomWriteCheckpoint({
419
+ checkpoint: 5n,
420
+ user_id: 'persistent'
421
+ });
422
+ writer.addCustomWriteCheckpoint({
423
+ checkpoint: 6n,
424
+ user_id: 'temporary',
425
+ checkpoint_requested_at: new Date('2024-01-01T00:00:00.000Z')
426
+ });
427
+ await writer.flush();
428
+ await writer.keepalive('1/1');
429
+
430
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'persistent' })).resolves.toEqual(5n);
431
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'temporary' })).resolves.toEqual(6n);
432
+
433
+ await compactActive(factory, {
434
+ compactBuckets: [],
435
+ deleteCheckpointRequestsBefore: new Date(Date.now() + 1_000)
436
+ });
437
+
438
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'persistent' })).resolves.toEqual(5n);
439
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'temporary' })).resolves.toBeNull();
440
+ }
441
+ );
442
+ }
443
+
444
+ async function createManagedWriteCheckpoint(
445
+ bucketStorage: storage.SyncRulesBucketStorage,
446
+ checkpoint: storage.ManagedWriteCheckpointOptions
447
+ ) {
448
+ const result = await createManagedWriteCheckpointResult(bucketStorage, checkpoint);
449
+ const writeCheckpoint = result.writeCheckpoints.get(checkpoint.user_id);
450
+ expect(writeCheckpoint).not.toBeUndefined();
451
+ return writeCheckpoint!;
452
+ }
453
+
454
+ async function createManagedWriteCheckpointResult(
455
+ bucketStorage: storage.SyncRulesBucketStorage,
456
+ checkpoint: storage.ManagedWriteCheckpointOptions
457
+ ) {
458
+ const result = await bucketStorage.createManagedWriteCheckpoints([checkpoint]);
459
+ expect(result.writeCheckpoints.get(checkpoint.user_id)).not.toBeUndefined();
460
+ return result;
304
461
  }