@powersync/service-core 0.0.0-dev-20250813080357 → 0.0.0-dev-20250819134004

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 (61) hide show
  1. package/CHANGELOG.md +10 -6
  2. package/dist/api/diagnostics.js +1 -1
  3. package/dist/api/diagnostics.js.map +1 -1
  4. package/dist/events/EventsEngine.d.ts +11 -0
  5. package/dist/{emitters/EmitterEngine.js → events/EventsEngine.js} +2 -2
  6. package/dist/events/EventsEngine.js.map +1 -0
  7. package/dist/routes/configure-fastify.d.ts +40 -0
  8. package/dist/routes/endpoints/socket-route.js +4 -7
  9. package/dist/routes/endpoints/socket-route.js.map +1 -1
  10. package/dist/routes/endpoints/sync-rules.js +1 -27
  11. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  12. package/dist/routes/endpoints/sync-stream.d.ts +80 -0
  13. package/dist/routes/endpoints/sync-stream.js +7 -11
  14. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  15. package/dist/storage/ReportStorage.d.ts +8 -0
  16. package/dist/storage/ReportStorage.js +2 -0
  17. package/dist/storage/ReportStorage.js.map +1 -0
  18. package/dist/storage/StorageEngine.d.ts +2 -2
  19. package/dist/storage/StorageEngine.js.map +1 -1
  20. package/dist/storage/StorageProvider.d.ts +3 -3
  21. package/dist/storage/storage-index.d.ts +1 -1
  22. package/dist/storage/storage-index.js +1 -1
  23. package/dist/storage/storage-index.js.map +1 -1
  24. package/dist/sync/BucketChecksumState.d.ts +34 -8
  25. package/dist/sync/BucketChecksumState.js +153 -18
  26. package/dist/sync/BucketChecksumState.js.map +1 -1
  27. package/dist/sync/sync.d.ts +1 -2
  28. package/dist/sync/sync.js +8 -10
  29. package/dist/sync/sync.js.map +1 -1
  30. package/dist/system/ServiceContext.d.ts +3 -3
  31. package/dist/system/ServiceContext.js +7 -4
  32. package/dist/system/ServiceContext.js.map +1 -1
  33. package/dist/util/protocol-types.d.ts +153 -5
  34. package/dist/util/protocol-types.js +41 -2
  35. package/dist/util/protocol-types.js.map +1 -1
  36. package/package.json +6 -6
  37. package/src/api/diagnostics.ts +1 -1
  38. package/src/{emitters/EmitterEngine.ts → events/EventsEngine.ts} +5 -6
  39. package/src/routes/endpoints/socket-route.ts +5 -9
  40. package/src/routes/endpoints/sync-rules.ts +1 -28
  41. package/src/routes/endpoints/sync-stream.ts +8 -13
  42. package/src/storage/ReportStorage.ts +11 -0
  43. package/src/storage/StorageEngine.ts +3 -3
  44. package/src/storage/StorageProvider.ts +3 -4
  45. package/src/storage/storage-index.ts +1 -1
  46. package/src/sync/BucketChecksumState.ts +183 -26
  47. package/src/sync/sync.ts +15 -13
  48. package/src/system/ServiceContext.ts +8 -5
  49. package/src/util/protocol-types.ts +138 -5
  50. package/test/src/sync/BucketChecksumState.test.ts +366 -34
  51. package/tsconfig.tsbuildinfo +1 -1
  52. package/dist/emitters/EmitterEngine.d.ts +0 -12
  53. package/dist/emitters/EmitterEngine.js.map +0 -1
  54. package/dist/emitters/emitter-interfaces.d.ts +0 -8
  55. package/dist/emitters/emitter-interfaces.js +0 -2
  56. package/dist/emitters/emitter-interfaces.js.map +0 -1
  57. package/dist/storage/ReportStorageFactory.d.ts +0 -8
  58. package/dist/storage/ReportStorageFactory.js +0 -2
  59. package/dist/storage/ReportStorageFactory.js.map +0 -1
  60. package/src/emitters/emitter-interfaces.ts +0 -12
  61. package/src/storage/ReportStorageFactory.ts +0 -9
@@ -1,17 +1,27 @@
1
1
  import {
2
2
  BucketChecksum,
3
3
  BucketChecksumState,
4
+ BucketChecksumStateOptions,
4
5
  BucketChecksumStateStorage,
5
6
  CHECKPOINT_INVALIDATE_ALL,
6
7
  ChecksumMap,
7
8
  InternalOpId,
8
9
  ReplicationCheckpoint,
10
+ StreamingSyncRequest,
9
11
  SyncContext,
10
12
  WatchFilterEvent
11
13
  } from '@/index.js';
12
14
  import { JSONBig } from '@powersync/service-jsonbig';
13
- import { ParameterLookup, RequestParameters, SqliteJsonRow, SqlSyncRules } from '@powersync/service-sync-rules';
14
- import { describe, expect, test } from 'vitest';
15
+ import {
16
+ SqliteJsonRow,
17
+ ParameterLookup,
18
+ SqlSyncRules,
19
+ RequestJwtPayload,
20
+ BucketSource,
21
+ BucketSourceType,
22
+ BucketParameterQuerier
23
+ } from '@powersync/service-sync-rules';
24
+ import { describe, expect, test, beforeEach } from 'vitest';
15
25
 
16
26
  describe('BucketChecksumState', () => {
17
27
  // Single global[] bucket.
@@ -55,6 +65,9 @@ bucket_definitions:
55
65
  maxDataFetchConcurrency: 10
56
66
  });
57
67
 
68
+ const syncRequest: StreamingSyncRequest = {};
69
+ const tokenPayload: RequestJwtPayload = { sub: '' };
70
+
58
71
  test('global bucket with update', async () => {
59
72
  const storage = new MockBucketChecksumStateStorage();
60
73
  // Set intial state
@@ -62,7 +75,8 @@ bucket_definitions:
62
75
 
63
76
  const state = new BucketChecksumState({
64
77
  syncContext,
65
- syncParams: new RequestParameters({ sub: '' }, {}),
78
+ syncRequest,
79
+ tokenPayload,
66
80
  syncRules: SYNC_RULES_GLOBAL,
67
81
  bucketStorage: storage
68
82
  });
@@ -75,9 +89,10 @@ bucket_definitions:
75
89
  line.advance();
76
90
  expect(line.checkpointLine).toEqual({
77
91
  checkpoint: {
78
- buckets: [{ bucket: 'global[]', checksum: 1, count: 1, priority: 3 }],
92
+ buckets: [{ bucket: 'global[]', checksum: 1, count: 1, priority: 3, subscriptions: [{ default: 0 }] }],
79
93
  last_op_id: '1',
80
- write_checkpoint: undefined
94
+ write_checkpoint: undefined,
95
+ streams: [{ name: 'global', is_default: true, errors: [] }]
81
96
  }
82
97
  });
83
98
  expect(line.bucketsToFetch).toEqual([
@@ -111,7 +126,7 @@ bucket_definitions:
111
126
  expect(line2.checkpointLine).toEqual({
112
127
  checkpoint_diff: {
113
128
  removed_buckets: [],
114
- updated_buckets: [{ bucket: 'global[]', checksum: 2, count: 2, priority: 3 }],
129
+ updated_buckets: [{ bucket: 'global[]', checksum: 2, count: 2, priority: 3, subscriptions: [{ default: 0 }] }],
115
130
  last_op_id: '2',
116
131
  write_checkpoint: undefined
117
132
  }
@@ -129,9 +144,9 @@ bucket_definitions:
129
144
 
130
145
  const state = new BucketChecksumState({
131
146
  syncContext,
147
+ tokenPayload,
132
148
  // Client sets the initial state here
133
- initialBucketPositions: [{ name: 'global[]', after: 1n }],
134
- syncParams: new RequestParameters({ sub: '' }, {}),
149
+ syncRequest: { buckets: [{ name: 'global[]', after: '1' }] },
135
150
  syncRules: SYNC_RULES_GLOBAL,
136
151
  bucketStorage: storage
137
152
  });
@@ -144,9 +159,10 @@ bucket_definitions:
144
159
  line.advance();
145
160
  expect(line.checkpointLine).toEqual({
146
161
  checkpoint: {
147
- buckets: [{ bucket: 'global[]', checksum: 1, count: 1, priority: 3 }],
162
+ buckets: [{ bucket: 'global[]', checksum: 1, count: 1, priority: 3, subscriptions: [{ default: 0 }] }],
148
163
  last_op_id: '1',
149
- write_checkpoint: undefined
164
+ write_checkpoint: undefined,
165
+ streams: [{ name: 'global', is_default: true, errors: [] }]
150
166
  }
151
167
  });
152
168
  expect(line.bucketsToFetch).toEqual([
@@ -167,7 +183,8 @@ bucket_definitions:
167
183
 
168
184
  const state = new BucketChecksumState({
169
185
  syncContext,
170
- syncParams: new RequestParameters({ sub: '' }, {}),
186
+ tokenPayload,
187
+ syncRequest,
171
188
  syncRules: SYNC_RULES_GLOBAL_TWO,
172
189
  bucketStorage: storage
173
190
  });
@@ -180,11 +197,12 @@ bucket_definitions:
180
197
  expect(line.checkpointLine).toEqual({
181
198
  checkpoint: {
182
199
  buckets: [
183
- { bucket: 'global[1]', checksum: 1, count: 1, priority: 3 },
184
- { bucket: 'global[2]', checksum: 1, count: 1, priority: 3 }
200
+ { bucket: 'global[1]', checksum: 1, count: 1, priority: 3, subscriptions: [{ default: 0 }] },
201
+ { bucket: 'global[2]', checksum: 1, count: 1, priority: 3, subscriptions: [{ default: 0 }] }
185
202
  ],
186
203
  last_op_id: '1',
187
- write_checkpoint: undefined
204
+ write_checkpoint: undefined,
205
+ streams: [{ name: 'global', is_default: true, errors: [] }]
188
206
  }
189
207
  });
190
208
  expect(line.bucketsToFetch).toEqual([
@@ -215,8 +233,8 @@ bucket_definitions:
215
233
  checkpoint_diff: {
216
234
  removed_buckets: [],
217
235
  updated_buckets: [
218
- { bucket: 'global[1]', checksum: 2, count: 2, priority: 3 },
219
- { bucket: 'global[2]', checksum: 2, count: 2, priority: 3 }
236
+ { bucket: 'global[1]', checksum: 2, count: 2, priority: 3, subscriptions: [{ default: 0 }] },
237
+ { bucket: 'global[2]', checksum: 2, count: 2, priority: 3, subscriptions: [{ default: 0 }] }
220
238
  ],
221
239
  last_op_id: '2',
222
240
  write_checkpoint: undefined
@@ -232,9 +250,9 @@ bucket_definitions:
232
250
 
233
251
  const state = new BucketChecksumState({
234
252
  syncContext,
253
+ tokenPayload,
235
254
  // Client sets the initial state here
236
- initialBucketPositions: [{ name: 'something_here[]', after: 1n }],
237
- syncParams: new RequestParameters({ sub: '' }, {}),
255
+ syncRequest: { buckets: [{ name: 'something_here[]', after: '1' }] },
238
256
  syncRules: SYNC_RULES_GLOBAL,
239
257
  bucketStorage: storage
240
258
  });
@@ -249,9 +267,10 @@ bucket_definitions:
249
267
  line.advance();
250
268
  expect(line.checkpointLine).toEqual({
251
269
  checkpoint: {
252
- buckets: [{ bucket: 'global[]', checksum: 1, count: 1, priority: 3 }],
270
+ buckets: [{ bucket: 'global[]', checksum: 1, count: 1, priority: 3, subscriptions: [{ default: 0 }] }],
253
271
  last_op_id: '1',
254
- write_checkpoint: undefined
272
+ write_checkpoint: undefined,
273
+ streams: [{ name: 'global', is_default: true, errors: [] }]
255
274
  }
256
275
  });
257
276
  expect(line.bucketsToFetch).toEqual([
@@ -273,7 +292,8 @@ bucket_definitions:
273
292
 
274
293
  const state = new BucketChecksumState({
275
294
  syncContext,
276
- syncParams: new RequestParameters({ sub: '' }, {}),
295
+ tokenPayload,
296
+ syncRequest,
277
297
  syncRules: SYNC_RULES_GLOBAL_TWO,
278
298
  bucketStorage: storage
279
299
  });
@@ -310,7 +330,7 @@ bucket_definitions:
310
330
  removed_buckets: [],
311
331
  updated_buckets: [
312
332
  // This does not include global[2], since it was not invalidated.
313
- { bucket: 'global[1]', checksum: 2, count: 2, priority: 3 }
333
+ { bucket: 'global[1]', checksum: 2, count: 2, priority: 3, subscriptions: [{ default: 0 }] }
314
334
  ],
315
335
  last_op_id: '2',
316
336
  write_checkpoint: undefined
@@ -325,7 +345,8 @@ bucket_definitions:
325
345
 
326
346
  const state = new BucketChecksumState({
327
347
  syncContext,
328
- syncParams: new RequestParameters({ sub: '' }, {}),
348
+ tokenPayload,
349
+ syncRequest,
329
350
  syncRules: SYNC_RULES_GLOBAL_TWO,
330
351
  bucketStorage: storage
331
352
  });
@@ -358,8 +379,8 @@ bucket_definitions:
358
379
  checkpoint_diff: {
359
380
  removed_buckets: [],
360
381
  updated_buckets: [
361
- { bucket: 'global[1]', checksum: 2, count: 2, priority: 3 },
362
- { bucket: 'global[2]', checksum: 2, count: 2, priority: 3 }
382
+ { bucket: 'global[1]', checksum: 2, count: 2, priority: 3, subscriptions: [{ default: 0 }] },
383
+ { bucket: 'global[2]', checksum: 2, count: 2, priority: 3, subscriptions: [{ default: 0 }] }
363
384
  ],
364
385
  last_op_id: '2',
365
386
  write_checkpoint: undefined
@@ -379,7 +400,8 @@ bucket_definitions:
379
400
 
380
401
  const state = new BucketChecksumState({
381
402
  syncContext,
382
- syncParams: new RequestParameters({ sub: '' }, {}),
403
+ tokenPayload,
404
+ syncRequest,
383
405
  syncRules: SYNC_RULES_GLOBAL_TWO,
384
406
  bucketStorage: storage
385
407
  });
@@ -393,11 +415,12 @@ bucket_definitions:
393
415
  expect(line.checkpointLine).toEqual({
394
416
  checkpoint: {
395
417
  buckets: [
396
- { bucket: 'global[1]', checksum: 3, count: 3, priority: 3 },
397
- { bucket: 'global[2]', checksum: 3, count: 3, priority: 3 }
418
+ { bucket: 'global[1]', checksum: 3, count: 3, priority: 3, subscriptions: [{ default: 0 }] },
419
+ { bucket: 'global[2]', checksum: 3, count: 3, priority: 3, subscriptions: [{ default: 0 }] }
398
420
  ],
399
421
  last_op_id: '3',
400
- write_checkpoint: undefined
422
+ write_checkpoint: undefined,
423
+ streams: [{ name: 'global', is_default: true, errors: [] }]
401
424
  }
402
425
  });
403
426
  expect(line.bucketsToFetch).toEqual([
@@ -444,7 +467,8 @@ bucket_definitions:
444
467
  bucket: 'global[1]',
445
468
  checksum: 4,
446
469
  count: 4,
447
- priority: 3
470
+ priority: 3,
471
+ subscriptions: [{ default: 0 }]
448
472
  }
449
473
  ],
450
474
  last_op_id: '4',
@@ -480,7 +504,8 @@ bucket_definitions:
480
504
 
481
505
  const state = new BucketChecksumState({
482
506
  syncContext,
483
- syncParams: new RequestParameters({ sub: 'u1' }, {}),
507
+ tokenPayload: { sub: 'u1' },
508
+ syncRequest,
484
509
  syncRules: SYNC_RULES_DYNAMIC,
485
510
  bucketStorage: storage
486
511
  });
@@ -496,10 +521,29 @@ bucket_definitions:
496
521
  expect(line.checkpointLine).toEqual({
497
522
  checkpoint: {
498
523
  buckets: [
499
- { bucket: 'by_project[1]', checksum: 1, count: 1, priority: 3 },
500
- { bucket: 'by_project[2]', checksum: 1, count: 1, priority: 3 }
524
+ {
525
+ bucket: 'by_project[1]',
526
+ checksum: 1,
527
+ count: 1,
528
+ priority: 3,
529
+ subscriptions: [{ default: 0 }]
530
+ },
531
+ {
532
+ bucket: 'by_project[2]',
533
+ checksum: 1,
534
+ count: 1,
535
+ priority: 3,
536
+ subscriptions: [{ default: 0 }]
537
+ }
501
538
  ],
502
539
  last_op_id: '1',
540
+ streams: [
541
+ {
542
+ is_default: true,
543
+ name: 'by_project',
544
+ errors: []
545
+ }
546
+ ],
503
547
  write_checkpoint: undefined
504
548
  }
505
549
  });
@@ -544,13 +588,301 @@ bucket_definitions:
544
588
  expect(line2.checkpointLine).toEqual({
545
589
  checkpoint_diff: {
546
590
  removed_buckets: [],
547
- updated_buckets: [{ bucket: 'by_project[3]', checksum: 1, count: 1, priority: 3 }],
591
+ updated_buckets: [
592
+ {
593
+ bucket: 'by_project[3]',
594
+ checksum: 1,
595
+ count: 1,
596
+ priority: 3,
597
+ subscriptions: [{ default: 0 }]
598
+ }
599
+ ],
548
600
  last_op_id: '2',
549
601
  write_checkpoint: undefined
550
602
  }
551
603
  });
552
604
  expect(line2.getFilteredBucketPositions()).toEqual(new Map([['by_project[3]', 0n]]));
553
605
  });
606
+
607
+ describe('streams', () => {
608
+ let source: { -readonly [P in keyof BucketSource]: BucketSource[P] };
609
+ let storage: MockBucketChecksumStateStorage;
610
+ let staticBucketIds = ['stream|0[]'];
611
+
612
+ function checksumState(options?: Partial<BucketChecksumStateOptions>) {
613
+ const rules = new SqlSyncRules('');
614
+ rules.bucketSources.push(source);
615
+
616
+ return new BucketChecksumState({
617
+ syncContext,
618
+ syncRequest,
619
+ tokenPayload,
620
+ syncRules: rules,
621
+ bucketStorage: storage,
622
+ ...options
623
+ });
624
+ }
625
+
626
+ function createQuerier(ids: string[], subscription: number | null): BucketParameterQuerier {
627
+ return {
628
+ staticBuckets: ids.map((bucket) => ({
629
+ definition: 'stream',
630
+ inclusion_reasons: subscription == null ? ['default'] : [{ subscription }],
631
+ bucket,
632
+ priority: 3
633
+ })),
634
+ hasDynamicBuckets: false,
635
+ parameterQueryLookups: [],
636
+ queryDynamicBucketDescriptions: function (): never {
637
+ throw new Error('no dynamic buckets.');
638
+ }
639
+ };
640
+ }
641
+
642
+ beforeEach(() => {
643
+ // Currently using mocked streams before streams are actually implemented as parsable rules.
644
+ source = {
645
+ name: 'stream',
646
+ type: BucketSourceType.SYNC_STREAM,
647
+ subscribedToByDefault: false,
648
+ pushBucketParameterQueriers(result, options) {
649
+ // Create a fake querier that resolves the global stream["default"] bucket by default and allows extracting
650
+ // additional buckets from parameters.
651
+ const subscriptions = options.streams['stream'] ?? [];
652
+ if (!this.subscribedToByDefault && !subscriptions.length) {
653
+ return;
654
+ }
655
+
656
+ let hasExplicitDefaultSubscription = false;
657
+ for (const subscription of subscriptions) {
658
+ try {
659
+ let subscriptionParameters = [];
660
+
661
+ if (subscription.parameters != null) {
662
+ subscriptionParameters = JSON.parse(subscription.parameters['ids'] as string).map(
663
+ (e: string) => `stream["${e}"]`
664
+ );
665
+ } else {
666
+ hasExplicitDefaultSubscription = true;
667
+ }
668
+
669
+ result.queriers.push(createQuerier([...subscriptionParameters], subscription.opaque_id));
670
+ } catch (e) {
671
+ result.errors.push({
672
+ descriptor: 'stream',
673
+ subscription,
674
+ message: `Error evaluating bucket ids: ${e.message}`
675
+ });
676
+ }
677
+ }
678
+
679
+ // If the stream is subscribed to by default and there is no explicit subscription that would match the default
680
+ // subscription, also include the default querier.
681
+ if (this.subscribedToByDefault && !hasExplicitDefaultSubscription) {
682
+ result.queriers.push(createQuerier(['stream["default"]'], null));
683
+ }
684
+ }
685
+ } satisfies Partial<BucketSource> as any;
686
+
687
+ storage = new MockBucketChecksumStateStorage();
688
+ storage.updateTestChecksum({ bucket: 'stream["default"]', checksum: 1, count: 1 });
689
+ storage.updateTestChecksum({ bucket: 'stream["a"]', checksum: 1, count: 1 });
690
+ storage.updateTestChecksum({ bucket: 'stream["b"]', checksum: 1, count: 1 });
691
+ });
692
+
693
+ test('includes defaults', async () => {
694
+ source.subscribedToByDefault = true;
695
+ const state = checksumState();
696
+
697
+ const line = await state.buildNextCheckpointLine({
698
+ base: storage.makeCheckpoint(1n),
699
+ writeCheckpoint: null,
700
+ update: CHECKPOINT_INVALIDATE_ALL
701
+ })!;
702
+ line?.advance();
703
+ expect(line?.checkpointLine).toEqual({
704
+ checkpoint: {
705
+ buckets: [
706
+ { bucket: 'stream["default"]', checksum: 1, count: 1, priority: 3, subscriptions: [{ default: 0 }] }
707
+ ],
708
+ last_op_id: '1',
709
+ write_checkpoint: undefined,
710
+ streams: [{ name: 'stream', is_default: true, errors: [] }]
711
+ }
712
+ });
713
+ });
714
+
715
+ test('can exclude defaults', async () => {
716
+ source.subscribedToByDefault = true;
717
+ const state = checksumState({ syncRequest: { streams: { include_defaults: false, subscriptions: [] } } });
718
+
719
+ const line = await state.buildNextCheckpointLine({
720
+ base: storage.makeCheckpoint(1n),
721
+ writeCheckpoint: null,
722
+ update: CHECKPOINT_INVALIDATE_ALL
723
+ })!;
724
+ line?.advance();
725
+ expect(line?.checkpointLine).toEqual({
726
+ checkpoint: {
727
+ buckets: [],
728
+ last_op_id: '1',
729
+ write_checkpoint: undefined,
730
+ streams: []
731
+ }
732
+ });
733
+ });
734
+
735
+ test('custom subscriptions', async () => {
736
+ source.subscribedToByDefault = true;
737
+
738
+ const state = checksumState({
739
+ syncRequest: {
740
+ streams: {
741
+ subscriptions: [
742
+ { stream: 'stream', parameters: { ids: '["a"]' }, override_priority: null },
743
+ { stream: 'stream', parameters: { ids: '["b"]' }, override_priority: 1 }
744
+ ]
745
+ }
746
+ }
747
+ });
748
+
749
+ const line = await state.buildNextCheckpointLine({
750
+ base: storage.makeCheckpoint(1n),
751
+ writeCheckpoint: null,
752
+ update: CHECKPOINT_INVALIDATE_ALL
753
+ })!;
754
+ line?.advance();
755
+ expect(line?.checkpointLine).toEqual({
756
+ checkpoint: {
757
+ buckets: [
758
+ { bucket: 'stream["a"]', checksum: 1, count: 1, priority: 3, subscriptions: [{ sub: 0 }] },
759
+ { bucket: 'stream["b"]', checksum: 1, count: 1, priority: 1, subscriptions: [{ sub: 1 }] },
760
+ { bucket: 'stream["default"]', checksum: 1, count: 1, priority: 3, subscriptions: [{ default: 0 }] }
761
+ ],
762
+ last_op_id: '1',
763
+ write_checkpoint: undefined,
764
+ streams: [{ name: 'stream', is_default: true, errors: [] }]
765
+ }
766
+ });
767
+ });
768
+
769
+ test('overlap between custom subscriptions', async () => {
770
+ const state = checksumState({
771
+ syncRequest: {
772
+ streams: {
773
+ subscriptions: [
774
+ { stream: 'stream', parameters: { ids: '["a", "b"]' }, override_priority: null },
775
+ { stream: 'stream', parameters: { ids: '["b"]' }, override_priority: 1 }
776
+ ]
777
+ }
778
+ }
779
+ });
780
+
781
+ const line = await state.buildNextCheckpointLine({
782
+ base: storage.makeCheckpoint(1n),
783
+ writeCheckpoint: null,
784
+ update: CHECKPOINT_INVALIDATE_ALL
785
+ })!;
786
+ line?.advance();
787
+ expect(line?.checkpointLine).toEqual({
788
+ checkpoint: {
789
+ buckets: [
790
+ { bucket: 'stream["a"]', checksum: 1, count: 1, priority: 3, subscriptions: [{ sub: 0 }] },
791
+ { bucket: 'stream["b"]', checksum: 1, count: 1, priority: 1, subscriptions: [{ sub: 0 }, { sub: 1 }] }
792
+ ],
793
+ last_op_id: '1',
794
+ write_checkpoint: undefined,
795
+ streams: [{ name: 'stream', is_default: false, errors: [] }]
796
+ }
797
+ });
798
+ });
799
+
800
+ test('overlap between default and custom subscription', async () => {
801
+ source.subscribedToByDefault = true;
802
+ const state = checksumState({
803
+ syncRequest: {
804
+ streams: {
805
+ subscriptions: [{ stream: 'stream', parameters: { ids: '["a", "default"]' }, override_priority: 1 }]
806
+ }
807
+ }
808
+ });
809
+
810
+ const line = await state.buildNextCheckpointLine({
811
+ base: storage.makeCheckpoint(1n),
812
+ writeCheckpoint: null,
813
+ update: CHECKPOINT_INVALIDATE_ALL
814
+ })!;
815
+ line?.advance();
816
+ expect(line?.checkpointLine).toEqual({
817
+ checkpoint: {
818
+ buckets: [
819
+ { bucket: 'stream["a"]', checksum: 1, count: 1, priority: 1, subscriptions: [{ sub: 0 }] },
820
+ {
821
+ bucket: 'stream["default"]',
822
+ checksum: 1,
823
+ count: 1,
824
+ priority: 1,
825
+ subscriptions: [{ sub: 0 }, { default: 0 }]
826
+ }
827
+ ],
828
+ last_op_id: '1',
829
+ write_checkpoint: undefined,
830
+ streams: [{ name: 'stream', is_default: true, errors: [] }]
831
+ }
832
+ });
833
+ });
834
+
835
+ test('reports errors', async () => {
836
+ source.subscribedToByDefault = true;
837
+
838
+ const state = checksumState({
839
+ syncRequest: {
840
+ streams: {
841
+ subscriptions: [
842
+ { stream: 'stream', parameters: { ids: '["a", "b"]' }, override_priority: 1 },
843
+ { stream: 'stream', parameters: { ids: 'invalid json' }, override_priority: null }
844
+ ]
845
+ }
846
+ }
847
+ });
848
+
849
+ const line = await state.buildNextCheckpointLine({
850
+ base: storage.makeCheckpoint(1n),
851
+ writeCheckpoint: null,
852
+ update: CHECKPOINT_INVALIDATE_ALL
853
+ })!;
854
+ line?.advance();
855
+ expect(line?.checkpointLine).toEqual({
856
+ checkpoint: {
857
+ buckets: [
858
+ { bucket: 'stream["a"]', checksum: 1, count: 1, priority: 1, subscriptions: [{ sub: 0 }] },
859
+ { bucket: 'stream["b"]', checksum: 1, count: 1, priority: 1, subscriptions: [{ sub: 0 }] },
860
+ {
861
+ bucket: 'stream["default"]',
862
+ checksum: 1,
863
+ count: 1,
864
+ priority: 3,
865
+ subscriptions: [{ default: 0 }]
866
+ }
867
+ ],
868
+ last_op_id: '1',
869
+ write_checkpoint: undefined,
870
+ streams: [
871
+ {
872
+ name: 'stream',
873
+ is_default: true,
874
+ errors: [
875
+ {
876
+ message: 'Error evaluating bucket ids: Unexpected token \'i\', "invalid json" is not valid JSON',
877
+ subscription: 1
878
+ }
879
+ ]
880
+ }
881
+ ]
882
+ }
883
+ });
884
+ });
885
+ });
554
886
  });
555
887
 
556
888
  class MockBucketChecksumStateStorage implements BucketChecksumStateStorage {