@powersync/service-core 1.20.5 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/api/RouteAPI.d.ts +3 -3
  3. package/dist/api/diagnostics.d.ts +1 -1
  4. package/dist/api/diagnostics.js +19 -3
  5. package/dist/api/diagnostics.js.map +1 -1
  6. package/dist/auth/RemoteJWKSCollector.js +3 -2
  7. package/dist/auth/RemoteJWKSCollector.js.map +1 -1
  8. package/dist/entry/commands/teardown-action.js +1 -1
  9. package/dist/entry/commands/teardown-action.js.map +1 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/modules/AbstractModule.d.ts +1 -1
  14. package/dist/replication/AbstractReplicationJob.js +1 -1
  15. package/dist/replication/AbstractReplicationJob.js.map +1 -1
  16. package/dist/replication/AbstractReplicator.d.ts +6 -6
  17. package/dist/replication/AbstractReplicator.js +21 -21
  18. package/dist/replication/AbstractReplicator.js.map +1 -1
  19. package/dist/replication/RelationCache.d.ts +9 -2
  20. package/dist/replication/RelationCache.js +21 -2
  21. package/dist/replication/RelationCache.js.map +1 -1
  22. package/dist/routes/configure-fastify.js +3 -1
  23. package/dist/routes/configure-fastify.js.map +1 -1
  24. package/dist/routes/endpoints/admin.js +16 -8
  25. package/dist/routes/endpoints/admin.js.map +1 -1
  26. package/dist/routes/endpoints/checkpointing.js +1 -1
  27. package/dist/routes/endpoints/checkpointing.js.map +1 -1
  28. package/dist/routes/endpoints/socket-route.js +1 -1
  29. package/dist/routes/endpoints/socket-route.js.map +1 -1
  30. package/dist/routes/endpoints/sync-rules.js +8 -8
  31. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  32. package/dist/routes/endpoints/sync-stream.js +2 -2
  33. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  34. package/dist/routes/route-register.d.ts +2 -0
  35. package/dist/routes/route-register.js +65 -3
  36. package/dist/routes/route-register.js.map +1 -1
  37. package/dist/runner/teardown.js +4 -4
  38. package/dist/runner/teardown.js.map +1 -1
  39. package/dist/storage/BucketStorage.d.ts +9 -9
  40. package/dist/storage/BucketStorage.js +9 -9
  41. package/dist/storage/BucketStorageBatch.d.ts +29 -0
  42. package/dist/storage/BucketStorageBatch.js.map +1 -1
  43. package/dist/storage/BucketStorageFactory.d.ts +27 -18
  44. package/dist/storage/BucketStorageFactory.js +13 -12
  45. package/dist/storage/BucketStorageFactory.js.map +1 -1
  46. package/dist/storage/PersistedSyncRulesContent.d.ts +6 -4
  47. package/dist/storage/PersistedSyncRulesContent.js +15 -8
  48. package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
  49. package/dist/storage/SourceEntity.d.ts +8 -1
  50. package/dist/storage/SourceTable.d.ts +32 -11
  51. package/dist/storage/SourceTable.js +41 -15
  52. package/dist/storage/SourceTable.js.map +1 -1
  53. package/dist/storage/StorageVersionConfig.d.ts +1 -1
  54. package/dist/storage/StorageVersionConfig.js +1 -1
  55. package/dist/storage/SyncRulesBucketStorage.d.ts +63 -18
  56. package/dist/storage/SyncRulesBucketStorage.js +14 -0
  57. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  58. package/dist/storage/WriteCheckpointAPI.d.ts +6 -6
  59. package/dist/storage/WriteCheckpointAPI.js +1 -1
  60. package/dist/storage/bson.d.ts +0 -1
  61. package/dist/storage/bson.js +0 -4
  62. package/dist/storage/bson.js.map +1 -1
  63. package/dist/sync/BucketChecksumState.d.ts +6 -9
  64. package/dist/sync/BucketChecksumState.js +117 -58
  65. package/dist/sync/BucketChecksumState.js.map +1 -1
  66. package/dist/sync/sync.d.ts +2 -2
  67. package/dist/sync/sync.js.map +1 -1
  68. package/dist/tracing/PerformanceTracer.d.ts +60 -0
  69. package/dist/tracing/PerformanceTracer.js +105 -0
  70. package/dist/tracing/PerformanceTracer.js.map +1 -0
  71. package/dist/tracing/TraceWriter.d.ts +22 -0
  72. package/dist/tracing/TraceWriter.js +63 -0
  73. package/dist/tracing/TraceWriter.js.map +1 -0
  74. package/dist/util/checkpointing.js +1 -1
  75. package/dist/util/config/compound-config-collector.d.ts +1 -1
  76. package/dist/util/config/compound-config-collector.js +2 -2
  77. package/dist/util/config/compound-config-collector.js.map +1 -1
  78. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +1 -1
  79. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
  80. package/dist/util/env.js +1 -1
  81. package/dist/util/protocol-types.d.ts +1 -1
  82. package/dist/util/protocol-types.js +1 -1
  83. package/dist/util/util-index.d.ts +1 -0
  84. package/dist/util/util-index.js +1 -0
  85. package/dist/util/util-index.js.map +1 -1
  86. package/dist/util/utils.d.ts +5 -0
  87. package/dist/util/utils.js +7 -0
  88. package/dist/util/utils.js.map +1 -1
  89. package/package.json +11 -11
  90. package/src/api/RouteAPI.ts +3 -3
  91. package/src/api/diagnostics.ts +29 -6
  92. package/src/auth/RemoteJWKSCollector.ts +3 -1
  93. package/src/entry/commands/teardown-action.ts +1 -1
  94. package/src/index.ts +2 -0
  95. package/src/modules/AbstractModule.ts +1 -1
  96. package/src/replication/AbstractReplicationJob.ts +1 -1
  97. package/src/replication/AbstractReplicator.ts +23 -23
  98. package/src/replication/RelationCache.ts +23 -4
  99. package/src/routes/configure-fastify.ts +8 -1
  100. package/src/routes/endpoints/admin.ts +17 -8
  101. package/src/routes/endpoints/checkpointing.ts +1 -1
  102. package/src/routes/endpoints/socket-route.ts +1 -1
  103. package/src/routes/endpoints/sync-rules.ts +8 -8
  104. package/src/routes/endpoints/sync-stream.ts +2 -2
  105. package/src/routes/route-register.ts +73 -4
  106. package/src/runner/teardown.ts +4 -4
  107. package/src/storage/BucketStorage.ts +9 -9
  108. package/src/storage/BucketStorageBatch.ts +32 -0
  109. package/src/storage/BucketStorageFactory.ts +35 -23
  110. package/src/storage/PersistedSyncRulesContent.ts +20 -12
  111. package/src/storage/SourceEntity.ts +9 -1
  112. package/src/storage/SourceTable.ts +56 -22
  113. package/src/storage/StorageVersionConfig.ts +1 -1
  114. package/src/storage/SyncRulesBucketStorage.ts +74 -22
  115. package/src/storage/WriteCheckpointAPI.ts +6 -6
  116. package/src/storage/bson.ts +0 -5
  117. package/src/sync/BucketChecksumState.ts +142 -78
  118. package/src/sync/sync.ts +4 -4
  119. package/src/tracing/PerformanceTracer.ts +149 -0
  120. package/src/tracing/TraceWriter.ts +67 -0
  121. package/src/util/checkpointing.ts +1 -1
  122. package/src/util/config/compound-config-collector.ts +3 -3
  123. package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +1 -1
  124. package/src/util/env.ts +1 -1
  125. package/src/util/protocol-types.ts +1 -1
  126. package/src/util/util-index.ts +1 -0
  127. package/src/util/utils.ts +8 -0
  128. package/test/src/auth.test.ts +120 -1
  129. package/test/src/diagnostics.test.ts +155 -0
  130. package/test/src/routes/error-handler.integration.test.ts +275 -0
  131. package/test/src/routes/stream.test.ts +15 -4
  132. package/test/src/storage/SourceTable.test.ts +89 -0
  133. package/test/src/sync/BucketChecksumState.test.ts +244 -80
  134. package/test/tsconfig.json +0 -1
  135. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,89 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import * as storage from '../../../src/storage/storage-index.js';
3
+
4
+ /**
5
+ * Build a SourceTable with the current `ref`-based options shape.
6
+ * Data/parameter sources are not relevant to the storeCurrentData behaviour, so they default to empty.
7
+ */
8
+ function makeTable(options: Partial<storage.SourceTableOptions> = {}): storage.SourceTable {
9
+ return new storage.SourceTable({
10
+ id: 'test-id',
11
+ ref: { connectionTag: 'test', schema: 'public', name: 'test_table' },
12
+ objectId: 123,
13
+ replicaIdColumns: [{ name: 'id' }],
14
+ snapshotComplete: true,
15
+ bucketDataSources: [],
16
+ parameterLookupSources: [],
17
+ ...options
18
+ });
19
+ }
20
+
21
+ describe('SourceTable', () => {
22
+ describe('storeCurrentData property', () => {
23
+ test('defaults to true', () => {
24
+ const table = makeTable();
25
+ expect(table.storeCurrentData).toBe(true);
26
+ });
27
+
28
+ test('can be set to false', () => {
29
+ const table = makeTable();
30
+ table.storeCurrentData = false;
31
+ expect(table.storeCurrentData).toBe(false);
32
+ });
33
+
34
+ test('is preserved when cloning', () => {
35
+ const table = makeTable();
36
+ table.storeCurrentData = false;
37
+ const cloned = table.clone();
38
+
39
+ expect(cloned.storeCurrentData).toBe(false);
40
+ expect(cloned).not.toBe(table);
41
+ });
42
+
43
+ test('clone preserves all properties including storeCurrentData', () => {
44
+ const table = makeTable({
45
+ replicaIdColumns: [{ name: 'id', type: 'int4' }],
46
+ snapshotComplete: false
47
+ });
48
+
49
+ table.syncData = false;
50
+ table.syncParameters = false;
51
+ table.syncEvent = false;
52
+ table.storeCurrentData = false;
53
+ table.snapshotStatus = {
54
+ totalEstimatedCount: 100,
55
+ replicatedCount: 50,
56
+ lastKey: Buffer.from('test')
57
+ };
58
+
59
+ const cloned = table.clone();
60
+
61
+ expect(cloned.syncData).toBe(false);
62
+ expect(cloned.syncParameters).toBe(false);
63
+ expect(cloned.syncEvent).toBe(false);
64
+ expect(cloned.storeCurrentData).toBe(false);
65
+ expect(cloned.snapshotStatus).toEqual(table.snapshotStatus);
66
+ });
67
+ });
68
+
69
+ describe('integration with other properties', () => {
70
+ test('storeCurrentData does not affect syncAny', () => {
71
+ const table = makeTable();
72
+
73
+ table.storeCurrentData = false;
74
+ table.syncData = true;
75
+ table.syncParameters = false;
76
+ table.syncEvent = false;
77
+
78
+ expect(table.syncAny).toBe(true); // Should still be true
79
+ });
80
+
81
+ test('storeCurrentData is independent of snapshot status', () => {
82
+ const table = makeTable({ snapshotComplete: false });
83
+
84
+ table.storeCurrentData = false;
85
+ expect(table.snapshotComplete).toBe(false);
86
+ expect(table.storeCurrentData).toBe(false);
87
+ });
88
+ });
89
+ });
@@ -7,6 +7,7 @@ import {
7
7
  ChecksumMap,
8
8
  InternalOpId,
9
9
  JwtPayload,
10
+ ParameterSetLimitExceededError,
10
11
  ReplicationCheckpoint,
11
12
  StreamingSyncRequest,
12
13
  SyncContext,
@@ -14,34 +15,39 @@ import {
14
15
  } from '@/index.js';
15
16
  import { JSONBig } from '@powersync/service-jsonbig';
16
17
  import {
18
+ nodeSqlite,
17
19
  ParameterIndexLookupCreator,
20
+ ParameterLookupDefinitionId,
21
+ ParameterLookupRows,
22
+ ParameterLookupScope,
18
23
  ScopedParameterLookup,
19
- SourceTableInterface,
20
- SqliteJsonRow,
21
- SqliteRow,
24
+ SourceTableRef,
22
25
  SqlSyncRules,
23
26
  TablePattern,
24
27
  versionedHydrationState
25
28
  } from '@powersync/service-sync-rules';
26
- import { ParameterLookupScope } from '@powersync/service-sync-rules/src/HydrationState.js';
29
+ import * as sqlite from 'node:sqlite';
27
30
  import { beforeEach, describe, expect, test } from 'vitest';
28
31
 
29
32
  describe('BucketChecksumState', () => {
30
33
  const LOOKUP_SOURCE: ParameterIndexLookupCreator = {
31
- get defaultLookupScope(): ParameterLookupScope {
34
+ get sourceId(): ParameterLookupDefinitionId {
32
35
  return {
33
36
  lookupName: 'lookup',
34
- queryId: '0',
35
- source: LOOKUP_SOURCE
37
+ queryId: '0'
36
38
  };
37
39
  },
38
40
  getSourceTables(): Set<TablePattern> {
39
41
  return new Set();
40
42
  },
41
- evaluateParameterRow(_sourceTable: SourceTableInterface, _row: SqliteRow) {
42
- return [];
43
+ createEvaluator() {
44
+ return {
45
+ evaluateParameterRow() {
46
+ return [];
47
+ }
48
+ };
43
49
  },
44
- tableSyncsParameters(_table: SourceTableInterface): boolean {
50
+ tableSyncsParameters(_table: SourceTableRef): boolean {
45
51
  return false;
46
52
  }
47
53
  };
@@ -59,7 +65,7 @@ bucket_definitions:
59
65
  data: []
60
66
  `,
61
67
  { defaultSchema: 'public' }
62
- ).config.hydrate({ hydrationState: versionedHydrationState(1) });
68
+ ).config.hydrate({ hydrationState: versionedHydrationState(1), sqlite: nodeSqlite(sqlite) });
63
69
 
64
70
  // global[1] and global[2]
65
71
  const SYNC_RULES_GLOBAL_TWO = SqlSyncRules.fromYaml(
@@ -72,7 +78,7 @@ bucket_definitions:
72
78
  data: []
73
79
  `,
74
80
  { defaultSchema: 'public' }
75
- ).config.hydrate({ hydrationState: versionedHydrationState(2) });
81
+ ).config.hydrate({ hydrationState: versionedHydrationState(2), sqlite: nodeSqlite(sqlite) });
76
82
 
77
83
  // by_project[n]
78
84
  const SYNC_RULES_DYNAMIC = SqlSyncRules.fromYaml(
@@ -83,7 +89,7 @@ bucket_definitions:
83
89
  data: []
84
90
  `,
85
91
  { defaultSchema: 'public' }
86
- ).config.hydrate({ hydrationState: versionedHydrationState(3) });
92
+ ).config.hydrate({ hydrationState: versionedHydrationState(3), sqlite: nodeSqlite(sqlite) });
87
93
 
88
94
  const syncContext = new SyncContext({
89
95
  maxBuckets: 100,
@@ -545,7 +551,7 @@ bucket_definitions:
545
551
  const line = (await state.buildNextCheckpointLine({
546
552
  base: storage.makeCheckpoint(1n, (lookups) => {
547
553
  expect(lookups).toEqual([ScopedParameterLookup.direct(lookupScope('by_project', '1'), ['u1'])]);
548
- return [{ id: 1 }, { id: 2 }];
554
+ return [{ lookup: lookups[0], rows: [{ id: 1 }, { id: 2 }] }];
549
555
  }),
550
556
  writeCheckpoint: null,
551
557
  update: CHECKPOINT_INVALIDATE_ALL
@@ -606,7 +612,7 @@ bucket_definitions:
606
612
  const line2 = (await state.buildNextCheckpointLine({
607
613
  base: storage.makeCheckpoint(2n, (lookups) => {
608
614
  expect(lookups).toEqual([ScopedParameterLookup.direct(lookupScope('by_project', '1'), ['u1'])]);
609
- return [{ id: 1 }, { id: 2 }, { id: 3 }];
615
+ return [{ lookup: lookups[0], rows: [{ id: 1 }, { id: 2 }, { id: 3 }] }];
610
616
  }),
611
617
  writeCheckpoint: null,
612
618
  update: {
@@ -654,7 +660,7 @@ config:
654
660
 
655
661
  const rules = SqlSyncRules.fromYaml(source, {
656
662
  defaultSchema: 'public'
657
- }).config.hydrate({ hydrationState: versionedHydrationState(1) });
663
+ }).config.hydrate({ hydrationState: versionedHydrationState(1), sqlite: nodeSqlite(sqlite) });
658
664
 
659
665
  return new BucketChecksumState({
660
666
  syncContext,
@@ -889,7 +895,8 @@ config:
889
895
 
890
896
  const line = (await state.buildNextCheckpointLine({
891
897
  base: storage.makeCheckpoint(1n, (lookups) => {
892
- return [{ id: 1 }, { id: 2 }, { id: 3 }];
898
+ expect(lookups).toHaveLength(1);
899
+ return [{ lookup: lookups[0], rows: [{ id: 1 }, { id: 2 }, { id: 3 }] }];
893
900
  }),
894
901
  writeCheckpoint: null,
895
902
  update: CHECKPOINT_INVALIDATE_ALL
@@ -900,22 +907,197 @@ config:
900
907
  expect(logData[0].parameter_query_results).toBe(3);
901
908
  });
902
909
 
903
- test('throws error with breakdown when parameter query limit is exceeded', async () => {
910
+ test('throws error with breakdown when too many parameters were fetched', async () => {
911
+ const syncRules = SqlSyncRules.fromYaml(
912
+ `
913
+ config:
914
+ edition: 3
915
+
916
+ streams:
917
+ a:
918
+ auto_subscribe: true
919
+ query: SELECT * FROM a WHERE id IN (SELECT id FROM magic_sequence WHERE count0 = auth.parameter('a'))
920
+ b:
921
+ auto_subscribe: true
922
+ query: SELECT * FROM a WHERE id IN (SELECT id FROM magic_sequence WHERE count1 = auth.parameter('b'))
923
+ c:
924
+ auto_subscribe: true
925
+ query: SELECT * FROM a WHERE id IN (SELECT id FROM magic_sequence WHERE count1 = auth.parameter('c'))
926
+ `,
927
+ { defaultSchema: 'public' }
928
+ ).config.hydrate({
929
+ hydrationState: versionedHydrationState(1),
930
+ sqlite: nodeSqlite(sqlite)
931
+ });
932
+
933
+ const storage = new MockBucketChecksumStateStorage();
934
+
935
+ const errorMessages: string[] = [];
936
+ const errorData: any[] = [];
937
+ const mockLogger = {
938
+ info: () => {},
939
+ error: (message: string, data: any) => {
940
+ errorMessages.push(message);
941
+ errorData.push(data);
942
+ },
943
+ warn: () => {},
944
+ debug: () => {}
945
+ };
946
+
947
+ const smallContext = new SyncContext({
948
+ maxBuckets: 100,
949
+ maxParameterQueryResults: 55,
950
+ maxDataFetchConcurrency: 10
951
+ });
952
+
953
+ const state = new BucketChecksumState({
954
+ syncContext: smallContext,
955
+ tokenPayload: new JwtPayload({
956
+ sub: 'u1',
957
+ // Create 60 total results: 30 a + 20 b + 10 c
958
+ a: BigInt(30),
959
+ b: BigInt(20),
960
+ c: BigInt(10)
961
+ }),
962
+ syncRequest,
963
+ syncRules,
964
+ bucketStorage: storage,
965
+ logger: mockLogger as any
966
+ });
967
+
968
+ await expect(
969
+ state.buildNextCheckpointLine({
970
+ base: storage.makeCheckpoint(1n, (lookups, limit) => {
971
+ expect(lookups).toHaveLength(1);
972
+ const [{ values }] = lookups;
973
+ expect(values[0]).toStrictEqual('lookup');
974
+
975
+ const count = Number(values[2]); // The count parameter from streams
976
+ if (count > limit) {
977
+ throw new ParameterSetLimitExceededError(limit);
978
+ }
979
+ return [{ lookup: lookups[0], rows: Array.from({ length: count }, (_, i) => ({ '0': BigInt(i) })) }];
980
+ }),
981
+ writeCheckpoint: null,
982
+ update: CHECKPOINT_INVALIDATE_ALL
983
+ })
984
+ ).rejects.toThrow('Too many parameter query results (limit of 55)');
985
+
986
+ // Verify error log includes breakdown
987
+ expect(errorMessages[0]).toContain('Invoked parameter queries by definition:');
988
+ expect(errorMessages[0]).toContain('Stream a evaluating parameter on magic_sequence: 30 results.');
989
+ expect(errorMessages[0]).toContain('Stream b evaluating parameter on magic_sequence: 20 results.');
990
+ expect(errorMessages[0]).toContain(
991
+ 'Stream c evaluating parameter on magic_sequence exceeded the remaining limit of 5 available results.'
992
+ );
993
+
994
+ expect(errorData[0].checkpoint).toEqual(1n);
995
+ expect(errorData[0].parameterResults).toEqual([
996
+ {
997
+ definition: 'Stream a evaluating parameter on magic_sequence',
998
+ didExceedLimit: false,
999
+ resultsOrLimit: 30
1000
+ },
1001
+ {
1002
+ definition: 'Stream b evaluating parameter on magic_sequence',
1003
+ didExceedLimit: false,
1004
+ resultsOrLimit: 20
1005
+ },
1006
+ {
1007
+ definition: 'Stream c evaluating parameter on magic_sequence',
1008
+ didExceedLimit: true,
1009
+ resultsOrLimit: 5
1010
+ }
1011
+ ]);
1012
+ });
1013
+
1014
+ test('throws error when too many lookups are requested at once', async () => {
1015
+ const syncRules = SqlSyncRules.fromYaml(
1016
+ `
1017
+ config:
1018
+ edition: 3
1019
+
1020
+ streams:
1021
+ a:
1022
+ auto_subscribe: true
1023
+ query: SELECT * FROM a WHERE x IN (SELECT x FROM b WHERE y IN auth.parameter('p'))
1024
+ `,
1025
+ { defaultSchema: 'public' }
1026
+ ).config.hydrate({
1027
+ hydrationState: versionedHydrationState(1),
1028
+ sqlite: nodeSqlite(sqlite)
1029
+ });
1030
+
1031
+ const storage = new MockBucketChecksumStateStorage();
1032
+
1033
+ const errorData: any[] = [];
1034
+ const mockLogger = {
1035
+ info: () => {},
1036
+ error: (_message: string, data: any) => {
1037
+ errorData.push(data);
1038
+ },
1039
+ warn: () => {},
1040
+ debug: () => {}
1041
+ };
1042
+
1043
+ const smallContext = new SyncContext({
1044
+ maxBuckets: 100,
1045
+ maxParameterQueryResults: 10,
1046
+ maxDataFetchConcurrency: 10
1047
+ });
1048
+
1049
+ const state = new BucketChecksumState({
1050
+ syncContext: smallContext,
1051
+ tokenPayload: new JwtPayload({
1052
+ sub: 'u1',
1053
+ p: Array.from({ length: 100 }, (_, i) => i)
1054
+ }),
1055
+ syncRequest,
1056
+ syncRules,
1057
+ bucketStorage: storage,
1058
+ logger: mockLogger as any
1059
+ });
1060
+
1061
+ await expect(
1062
+ state.buildNextCheckpointLine({
1063
+ base: storage.makeCheckpoint(1n, () => {
1064
+ throw new Error('should not get called');
1065
+ }),
1066
+ writeCheckpoint: null,
1067
+ update: CHECKPOINT_INVALIDATE_ALL
1068
+ })
1069
+ ).rejects.toThrow('Attempted to fetch 100 lookups at once, a maximum of 10 lookups are allowed');
1070
+
1071
+ expect(errorData).toStrictEqual([
1072
+ {
1073
+ user_id: 'u1',
1074
+ checkpoint: 1n,
1075
+ cause: 'Stream a evaluating parameter on b'
1076
+ }
1077
+ ]);
1078
+ });
1079
+
1080
+ test('throws error with breakdown when bucket limit is exceeded', async () => {
1081
+ // These streams are designed to return buckets without consuming too many parameters (as exceeding that limit is
1082
+ // a different error).
904
1083
  const SYNC_RULES_MULTI = SqlSyncRules.fromYaml(
905
1084
  `
906
- bucket_definitions:
1085
+ config:
1086
+ edition: 3
1087
+
1088
+ streams:
907
1089
  projects:
908
- parameters: select id from projects where user_id = request.user_id()
909
- data: []
1090
+ auto_subscribe: true
1091
+ query: SELECT id FROM projects WHERE p IN auth.parameter('a')
910
1092
  tasks:
911
- parameters: select id from tasks where user_id = request.user_id()
912
- data: []
1093
+ auto_subscribe: true
1094
+ query: SELECT id FROM tasks WHERE p IN auth.parameter('b')
913
1095
  comments:
914
- parameters: select id from comments where user_id = request.user_id()
915
- data: []
1096
+ auto_subscribe: true
1097
+ query: SELECT id FROM comments WHERE p IN auth.parameter('c')
916
1098
  `,
917
1099
  { defaultSchema: 'public' }
918
- ).config.hydrate({ hydrationState: versionedHydrationState(4) });
1100
+ ).config.hydrate({ hydrationState: versionedHydrationState(4), sqlite: nodeSqlite(sqlite) });
919
1101
 
920
1102
  const storage = new MockBucketChecksumStateStorage();
921
1103
 
@@ -932,79 +1114,66 @@ bucket_definitions:
932
1114
  };
933
1115
 
934
1116
  const smallContext = new SyncContext({
935
- maxBuckets: 100,
936
- maxParameterQueryResults: 50,
1117
+ maxBuckets: 50,
1118
+ maxParameterQueryResults: 10,
937
1119
  maxDataFetchConcurrency: 10
938
1120
  });
939
1121
 
940
1122
  const state = new BucketChecksumState({
941
1123
  syncContext: smallContext,
942
- tokenPayload: new JwtPayload({ sub: 'u1' }),
1124
+ tokenPayload: new JwtPayload({
1125
+ sub: 'u1',
1126
+ // Create 60 total results: 30 projects + 20 tasks + 10 comments
1127
+ a: Array.from({ length: 30 }, (_, i) => BigInt(i)),
1128
+ b: Array.from({ length: 20 }, (_, i) => BigInt(i)),
1129
+ c: Array.from({ length: 10 }, (_, i) => BigInt(i))
1130
+ }),
943
1131
  syncRequest,
944
1132
  syncRules: SYNC_RULES_MULTI,
945
1133
  bucketStorage: storage,
946
1134
  logger: mockLogger as any
947
1135
  });
948
1136
 
949
- // Create 60 total results: 30 projects + 20 tasks + 10 comments
950
- const projectIds = Array.from({ length: 30 }, (_, i) => ({ id: i + 1 }));
951
- const taskIds = Array.from({ length: 20 }, (_, i) => ({ id: i + 1 }));
952
- const commentIds = Array.from({ length: 10 }, (_, i) => ({ id: i + 1 }));
953
-
954
- for (let i = 1; i <= 30; i++) {
955
- storage.updateTestChecksum({ bucket: `projects[${i}]`, checksum: 1, count: 1 });
956
- }
957
- for (let i = 1; i <= 20; i++) {
958
- storage.updateTestChecksum({ bucket: `tasks[${i}]`, checksum: 1, count: 1 });
959
- }
960
- for (let i = 1; i <= 10; i++) {
961
- storage.updateTestChecksum({ bucket: `comments[${i}]`, checksum: 1, count: 1 });
962
- }
963
-
964
1137
  await expect(
965
1138
  state.buildNextCheckpointLine({
966
- base: storage.makeCheckpoint(1n, (lookups) => {
967
- const lookup = lookups[0];
968
- const lookupName = lookup.values[0];
969
- if (lookupName === 'projects') {
970
- return projectIds;
971
- } else if (lookupName === 'tasks') {
972
- return taskIds;
973
- } else {
974
- return commentIds;
975
- }
976
- }),
1139
+ base: storage.makeCheckpoint(1n),
977
1140
  writeCheckpoint: null,
978
1141
  update: CHECKPOINT_INVALIDATE_ALL
979
1142
  })
980
- ).rejects.toThrow('Too many parameter query results: 60 (limit of 50)');
1143
+ ).rejects.toThrow('Too many buckets: 60 (limit of 50');
981
1144
 
982
1145
  // Verify error log includes breakdown
983
- expect(errorMessages[0]).toContain('Parameter query results by definition:');
1146
+ expect(errorMessages[0]).toContain('Buckets by definition:');
984
1147
  expect(errorMessages[0]).toContain('projects: 30');
985
1148
  expect(errorMessages[0]).toContain('tasks: 20');
986
1149
  expect(errorMessages[0]).toContain('comments: 10');
987
1150
 
988
1151
  expect(errorData[0].checkpoint).toEqual(1n);
989
- expect(errorData[0].parameter_query_results).toBe(60);
990
- expect(errorData[0].parameter_query_results_by_definition).toEqual({
1152
+ expect(errorData[0].buckets).toBe(60);
1153
+ expect(errorData[0].buckets_by_definition).toEqual({
991
1154
  projects: 30,
992
1155
  tasks: 20,
993
1156
  comments: 10
994
1157
  });
995
1158
  });
996
1159
 
997
- test('limits breakdown to top 10 definitions', async () => {
998
- // Create sync rules with 15 different definitions with dynamic parameters
999
- let yamlDefinitions = 'bucket_definitions:\n';
1160
+ test('limits bucket breakdown to top 10 definitions', async () => {
1161
+ // Create sync streams with 15 different definitions with dynamic parameters
1162
+ let yamlDefinitions = `
1163
+ config:
1164
+ edition: 3
1165
+
1166
+ streams:
1167
+ `;
1000
1168
  for (let i = 1; i <= 15; i++) {
1001
1169
  yamlDefinitions += ` def${i}:\n`;
1002
- yamlDefinitions += ` parameters: select id from def${i}_table where user_id = request.user_id()\n`;
1003
- yamlDefinitions += ` data: []\n`;
1170
+ yamlDefinitions += ` auto_subscribe: true\n`;
1171
+ yamlDefinitions += ` query: SELECT * FROM tbl WHERE b = auth.parameter('${i}')\n`;
1004
1172
  }
1005
1173
 
1006
1174
  const SYNC_RULES_MANY = SqlSyncRules.fromYaml(yamlDefinitions, { defaultSchema: 'public' }).config.hydrate({
1007
- hydrationState: versionedHydrationState(5)
1175
+ hydrationState: versionedHydrationState(5),
1176
+ sqlite: nodeSqlite(sqlite)
1008
1177
  });
1009
1178
 
1010
1179
  const storage = new MockBucketChecksumStateStorage();
@@ -1020,35 +1189,30 @@ bucket_definitions:
1020
1189
  };
1021
1190
 
1022
1191
  const smallContext = new SyncContext({
1023
- maxBuckets: 100,
1192
+ maxBuckets: 10,
1024
1193
  maxParameterQueryResults: 10,
1025
1194
  maxDataFetchConcurrency: 10
1026
1195
  });
1027
1196
 
1028
1197
  const state = new BucketChecksumState({
1029
1198
  syncContext: smallContext,
1030
- tokenPayload: new JwtPayload({ sub: 'u1' }),
1199
+ tokenPayload: new JwtPayload({
1200
+ sub: 'u1',
1201
+ ...Object.fromEntries(Array.from({ length: 30 }, (_, i) => [i.toString(), BigInt(i)]))
1202
+ }),
1031
1203
  syncRequest,
1032
1204
  syncRules: SYNC_RULES_MANY,
1033
1205
  bucketStorage: storage,
1034
1206
  logger: mockLogger as any
1035
1207
  });
1036
1208
 
1037
- // Each definition creates one bucket, total 15 buckets
1038
- for (let i = 1; i <= 15; i++) {
1039
- storage.updateTestChecksum({ bucket: `def${i}[${i}]`, checksum: 1, count: 1 });
1040
- }
1041
-
1042
1209
  await expect(
1043
1210
  state.buildNextCheckpointLine({
1044
- base: storage.makeCheckpoint(1n, (lookups) => {
1045
- // Return one result for each definition
1046
- return [{ id: 1 }];
1047
- }),
1211
+ base: storage.makeCheckpoint(1n),
1048
1212
  writeCheckpoint: null,
1049
1213
  update: CHECKPOINT_INVALIDATE_ALL
1050
1214
  })
1051
- ).rejects.toThrow('Too many parameter query results: 15 (limit of 10)');
1215
+ ).rejects.toThrow('Too many buckets: 15 (limit of 10)');
1052
1216
 
1053
1217
  // Verify only top 10 are shown
1054
1218
  const errorMessage = errorMessages[0];
@@ -1094,16 +1258,16 @@ class MockBucketChecksumStateStorage implements BucketChecksumStateStorage {
1094
1258
 
1095
1259
  makeCheckpoint(
1096
1260
  opId: InternalOpId,
1097
- parameters?: (lookups: ScopedParameterLookup[]) => SqliteJsonRow[]
1261
+ parameters?: (lookups: ScopedParameterLookup[], limit: number) => ParameterLookupRows[]
1098
1262
  ): ReplicationCheckpoint {
1099
1263
  return {
1100
1264
  checkpoint: opId,
1101
1265
  lsn: String(opId),
1102
- getParameterSets: async (lookups: ScopedParameterLookup[]) => {
1266
+ getParameterSets: async (lookups: ScopedParameterLookup[], limit) => {
1103
1267
  if (parameters == null) {
1104
1268
  throw new Error(`getParametersSets not defined for checkpoint ${opId}`);
1105
1269
  }
1106
- return parameters(lookups);
1270
+ return parameters(lookups, limit);
1107
1271
  }
1108
1272
  };
1109
1273
  }
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "extends": "../../../tsconfig.tests.json",
3
3
  "compilerOptions": {
4
- "baseUrl": "./",
5
4
  "outDir": "dist",
6
5
  "paths": {
7
6
  "@/*": ["../src/*"]