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

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 (41) hide show
  1. package/CHANGELOG.md +75 -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/stream_utils.d.ts +0 -1
  14. package/dist/test-utils/stream_utils.js +0 -10
  15. package/dist/test-utils/stream_utils.js.map +1 -1
  16. package/dist/tests/register-compacting-tests.js +28 -21
  17. package/dist/tests/register-compacting-tests.js.map +1 -1
  18. package/dist/tests/register-data-storage-checkpoint-tests.js +102 -3
  19. package/dist/tests/register-data-storage-checkpoint-tests.js.map +1 -1
  20. package/dist/tests/register-data-storage-data-tests.js +202 -136
  21. package/dist/tests/register-data-storage-data-tests.js.map +1 -1
  22. package/dist/tests/register-data-storage-parameter-tests.js +44 -42
  23. package/dist/tests/register-data-storage-parameter-tests.js.map +1 -1
  24. package/dist/tests/register-sync-tests.js +399 -82
  25. package/dist/tests/register-sync-tests.js.map +1 -1
  26. package/dist/tests/util.d.ts +2 -2
  27. package/dist/tests/util.js.map +1 -1
  28. package/package.json +5 -6
  29. package/src/test-utils/AbstractStreamTestContext.ts +31 -27
  30. package/src/test-utils/StorageDataHelpers.ts +52 -8
  31. package/src/test-utils/bucket-validation.ts +3 -3
  32. package/src/test-utils/general-utils.ts +62 -10
  33. package/src/test-utils/stream_utils.ts +0 -11
  34. package/src/tests/register-compacting-tests.ts +47 -24
  35. package/src/tests/register-data-storage-checkpoint-tests.ts +132 -3
  36. package/src/tests/register-data-storage-data-tests.ts +248 -148
  37. package/src/tests/register-data-storage-parameter-tests.ts +58 -42
  38. package/src/tests/register-sync-tests.ts +438 -84
  39. package/src/tests/util.ts +2 -2
  40. package/tsconfig.json +1 -4
  41. package/tsconfig.tsbuildinfo +1 -1
@@ -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
  {
@@ -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([
@@ -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({
@@ -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);
@@ -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);
@@ -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({
@@ -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);
@@ -645,8 +665,11 @@ bucket_definitions:
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.
@@ -42,7 +42,7 @@ bucket_definitions:
42
42
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
43
43
  await writer.markAllSnapshotDone('1/1');
44
44
 
45
- const writeCheckpoint = await bucketStorage.createManagedWriteCheckpoint({
45
+ const writeCheckpoint = await createManagedWriteCheckpoint(bucketStorage, {
46
46
  heads: { '1': '5/0' },
47
47
  user_id: 'user1'
48
48
  });
@@ -102,12 +102,12 @@ bucket_definitions:
102
102
  }
103
103
  });
104
104
 
105
- const writeCheckpoint = await bucketStorage.createManagedWriteCheckpoint({
105
+ const writeCheckpoint = await createManagedWriteCheckpoint(bucketStorage, {
106
106
  heads: { '1': '6/0' },
107
107
  user_id: 'user1'
108
108
  });
109
109
  // 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().
110
+ // This is what is effectively triggered by RouteAPI.createReplicationHead().
111
111
  // MongoDB storage doesn't explicitly need this anymore.
112
112
  await writer.keepalive('6/0');
113
113
 
@@ -128,6 +128,72 @@ bucket_definitions:
128
128
  });
129
129
  });
130
130
 
131
+ test('managed write checkpoints - client supplied ids are monotonic', async () => {
132
+ await using factory = await generateStorageFactory();
133
+ const r = await factory.configureSyncRules(
134
+ updateSyncRulesFromYaml(
135
+ `
136
+ bucket_definitions:
137
+ mybucket:
138
+ data: []
139
+ `,
140
+ {
141
+ validate: false,
142
+ storageVersion
143
+ }
144
+ )
145
+ );
146
+ const bucketStorage = factory.getInstance(r.persisted_sync_rules!);
147
+
148
+ const first = await createManagedWriteCheckpointResult(bucketStorage, {
149
+ user_id: 'user1',
150
+ heads: { '1': '5/0' },
151
+ checkpoint_request_id: 42n
152
+ });
153
+ expect(first.writeCheckpoints.get('user1')).toEqual(42n);
154
+ expect(first.shouldAdvance).toBe(true);
155
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'user1', heads: { '1': '5/0' } })).resolves.toEqual(42n);
156
+
157
+ const stale = await createManagedWriteCheckpointResult(bucketStorage, {
158
+ user_id: 'user1',
159
+ heads: { '1': '8/0' },
160
+ checkpoint_request_id: 41n
161
+ });
162
+ expect(stale.writeCheckpoints.get('user1')).toEqual(42n);
163
+ // The stored checkpoint is unchanged but has not been processed by replication
164
+ // yet, so the source marker must still be forced (e.g. for a retry after a
165
+ // previous attempt failed to advance the marker).
166
+ expect(stale.shouldAdvance).toBe(true);
167
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'user1', heads: { '1': '6/0' } })).resolves.toEqual(42n);
168
+
169
+ const retried = await createManagedWriteCheckpointResult(bucketStorage, {
170
+ user_id: 'user1',
171
+ heads: { '1': '8/0' },
172
+ checkpoint_request_id: 42n
173
+ });
174
+ expect(retried.writeCheckpoints.get('user1')).toEqual(42n);
175
+ // Same as above: the duplicate request still points at a pending checkpoint.
176
+ expect(retried.shouldAdvance).toBe(true);
177
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'user1', heads: { '1': '6/0' } })).resolves.toEqual(42n);
178
+
179
+ const advanced = await createManagedWriteCheckpointResult(bucketStorage, {
180
+ user_id: 'user1',
181
+ heads: { '1': '8/0' },
182
+ checkpoint_request_id: 43n
183
+ });
184
+ expect(advanced.writeCheckpoints.get('user1')).toEqual(43n);
185
+ expect(advanced.shouldAdvance).toBe(true);
186
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'user1', heads: { '1': '7/0' } })).resolves.toBeNull();
187
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'user1', heads: { '1': '8/0' } })).resolves.toEqual(43n);
188
+
189
+ const generated = await createManagedWriteCheckpointResult(bucketStorage, {
190
+ user_id: 'user1',
191
+ heads: { '1': '9/0' }
192
+ });
193
+ expect(generated.writeCheckpoints.get('user1')).toEqual(44n);
194
+ expect(generated.shouldAdvance).toBe(true);
195
+ });
196
+
131
197
  test('custom write checkpoints - checkpoint after write', async (context) => {
132
198
  await using factory = await generateStorageFactory();
133
199
  const r = await factory.configureSyncRules(
@@ -301,4 +367,67 @@ bucket_definitions:
301
367
  }
302
368
  });
303
369
  });
370
+
371
+ test('custom write checkpoints - checkpoint request markers are temporary', async () => {
372
+ await using factory = await generateStorageFactory();
373
+ const r = await factory.configureSyncRules(
374
+ updateSyncRulesFromYaml(
375
+ `
376
+ bucket_definitions:
377
+ mybucket:
378
+ data: []
379
+ `,
380
+ {
381
+ validate: false,
382
+ storageVersion
383
+ }
384
+ )
385
+ );
386
+ const bucketStorage = factory.getInstance(r.persisted_sync_rules!);
387
+ bucketStorage.setWriteCheckpointMode(storage.WriteCheckpointMode.CUSTOM);
388
+
389
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
390
+ await writer.markAllSnapshotDone('1/1');
391
+
392
+ writer.addCustomWriteCheckpoint({
393
+ checkpoint: 5n,
394
+ user_id: 'persistent'
395
+ });
396
+ writer.addCustomWriteCheckpoint({
397
+ checkpoint: 6n,
398
+ user_id: 'temporary',
399
+ checkpoint_requested_at: new Date('2024-01-01T00:00:00.000Z')
400
+ });
401
+ await writer.flush();
402
+
403
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'persistent' })).resolves.toEqual(5n);
404
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'temporary' })).resolves.toEqual(6n);
405
+
406
+ await bucketStorage.compact({
407
+ compactBuckets: [],
408
+ deleteCheckpointRequestsBefore: new Date(Date.now() + 1_000)
409
+ });
410
+
411
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'persistent' })).resolves.toEqual(5n);
412
+ await expect(bucketStorage.lastWriteCheckpoint({ user_id: 'temporary' })).resolves.toBeNull();
413
+ });
414
+ }
415
+
416
+ async function createManagedWriteCheckpoint(
417
+ bucketStorage: storage.SyncRulesBucketStorage,
418
+ checkpoint: storage.ManagedWriteCheckpointOptions
419
+ ) {
420
+ const result = await createManagedWriteCheckpointResult(bucketStorage, checkpoint);
421
+ const writeCheckpoint = result.writeCheckpoints.get(checkpoint.user_id);
422
+ expect(writeCheckpoint).not.toBeUndefined();
423
+ return writeCheckpoint!;
424
+ }
425
+
426
+ async function createManagedWriteCheckpointResult(
427
+ bucketStorage: storage.SyncRulesBucketStorage,
428
+ checkpoint: storage.ManagedWriteCheckpointOptions
429
+ ) {
430
+ const result = await bucketStorage.createManagedWriteCheckpoints([checkpoint]);
431
+ expect(result.writeCheckpoints.get(checkpoint.user_id)).not.toBeUndefined();
432
+ return result;
304
433
  }