@powersync/service-core 0.0.0-dev-20260225160713 → 0.0.0-dev-20260511080634

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 (246) hide show
  1. package/CHANGELOG.md +102 -5
  2. package/dist/api/RouteAPI.d.ts +17 -3
  3. package/dist/api/api-index.d.ts +1 -1
  4. package/dist/api/api-index.js +1 -1
  5. package/dist/api/api-index.js.map +1 -1
  6. package/dist/api/api-metrics.js.map +1 -1
  7. package/dist/api/diagnostics.d.ts +1 -1
  8. package/dist/api/diagnostics.js +32 -14
  9. package/dist/api/diagnostics.js.map +1 -1
  10. package/dist/auth/CachedKeyCollector.js +1 -1
  11. package/dist/auth/CachedKeyCollector.js.map +1 -1
  12. package/dist/auth/CompoundKeyCollector.js.map +1 -1
  13. package/dist/auth/KeyStore.js.map +1 -1
  14. package/dist/auth/RemoteJWKSCollector.js.map +1 -1
  15. package/dist/auth/StaticKeyCollector.d.ts +1 -1
  16. package/dist/auth/StaticKeyCollector.js.map +1 -1
  17. package/dist/auth/StaticSupabaseKeyCollector.d.ts +1 -1
  18. package/dist/auth/StaticSupabaseKeyCollector.js.map +1 -1
  19. package/dist/auth/utils.d.ts +2 -1
  20. package/dist/auth/utils.js +1 -1
  21. package/dist/auth/utils.js.map +1 -1
  22. package/dist/entry/commands/compact-action.js +26 -5
  23. package/dist/entry/commands/compact-action.js.map +1 -1
  24. package/dist/entry/commands/teardown-action.js +2 -2
  25. package/dist/entry/commands/teardown-action.js.map +1 -1
  26. package/dist/entry/entry-index.d.ts +1 -1
  27. package/dist/entry/entry-index.js +1 -1
  28. package/dist/entry/entry-index.js.map +1 -1
  29. package/dist/events/EventsEngine.js +1 -1
  30. package/dist/events/EventsEngine.js.map +1 -1
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +1 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/metrics/MetricsEngine.d.ts +1 -1
  35. package/dist/metrics/RollingBucketMax.d.ts +28 -0
  36. package/dist/metrics/RollingBucketMax.js +80 -0
  37. package/dist/metrics/RollingBucketMax.js.map +1 -0
  38. package/dist/metrics/metrics-index.d.ts +3 -2
  39. package/dist/metrics/metrics-index.js +3 -2
  40. package/dist/metrics/metrics-index.js.map +1 -1
  41. package/dist/metrics/open-telemetry/util.js +1 -1
  42. package/dist/metrics/open-telemetry/util.js.map +1 -1
  43. package/dist/metrics/register-metrics.js +2 -2
  44. package/dist/metrics/register-metrics.js.map +1 -1
  45. package/dist/modules/AbstractModule.d.ts +2 -2
  46. package/dist/modules/AbstractModule.js.map +1 -1
  47. package/dist/modules/modules-index.d.ts +1 -1
  48. package/dist/modules/modules-index.js +1 -1
  49. package/dist/modules/modules-index.js.map +1 -1
  50. package/dist/replication/AbstractReplicationJob.d.ts +2 -2
  51. package/dist/replication/AbstractReplicationJob.js +1 -1
  52. package/dist/replication/AbstractReplicationJob.js.map +1 -1
  53. package/dist/replication/AbstractReplicator.d.ts +7 -7
  54. package/dist/replication/AbstractReplicator.js +31 -28
  55. package/dist/replication/AbstractReplicator.js.map +1 -1
  56. package/dist/replication/ReplicationLagTracker.d.ts +50 -0
  57. package/dist/replication/ReplicationLagTracker.js +78 -0
  58. package/dist/replication/ReplicationLagTracker.js.map +1 -0
  59. package/dist/replication/replication-index.d.ts +3 -2
  60. package/dist/replication/replication-index.js +3 -2
  61. package/dist/replication/replication-index.js.map +1 -1
  62. package/dist/replication/replication-metrics.js.map +1 -1
  63. package/dist/routes/configure-fastify.d.ts +59 -32
  64. package/dist/routes/endpoints/admin.d.ts +108 -54
  65. package/dist/routes/endpoints/admin.js +8 -3
  66. package/dist/routes/endpoints/admin.js.map +1 -1
  67. package/dist/routes/endpoints/checkpointing.js +1 -1
  68. package/dist/routes/endpoints/checkpointing.js.map +1 -1
  69. package/dist/routes/endpoints/socket-route.js +1 -1
  70. package/dist/routes/endpoints/socket-route.js.map +1 -1
  71. package/dist/routes/endpoints/sync-rules.js +10 -10
  72. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  73. package/dist/routes/endpoints/sync-stream.d.ts +10 -10
  74. package/dist/routes/endpoints/sync-stream.js +8 -3
  75. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  76. package/dist/routes/hooks.js +1 -1
  77. package/dist/routes/hooks.js.map +1 -1
  78. package/dist/routes/route-register.js.map +1 -1
  79. package/dist/runner/teardown.js +4 -4
  80. package/dist/runner/teardown.js.map +1 -1
  81. package/dist/storage/BucketStorage.d.ts +9 -9
  82. package/dist/storage/BucketStorage.js +9 -9
  83. package/dist/storage/BucketStorageBatch.d.ts +30 -9
  84. package/dist/storage/BucketStorageBatch.js.map +1 -1
  85. package/dist/storage/BucketStorageFactory.d.ts +32 -20
  86. package/dist/storage/BucketStorageFactory.js +19 -16
  87. package/dist/storage/BucketStorageFactory.js.map +1 -1
  88. package/dist/storage/ChecksumCache.d.ts +5 -2
  89. package/dist/storage/ChecksumCache.js +8 -4
  90. package/dist/storage/ChecksumCache.js.map +1 -1
  91. package/dist/storage/PersistedSyncRulesContent.d.ts +9 -3
  92. package/dist/storage/PersistedSyncRulesContent.js +25 -5
  93. package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
  94. package/dist/storage/ReplicationEventPayload.d.ts +1 -1
  95. package/dist/storage/SourceTable.d.ts +10 -5
  96. package/dist/storage/SourceTable.js +3 -3
  97. package/dist/storage/SourceTable.js.map +1 -1
  98. package/dist/storage/StorageVersionConfig.d.ts +34 -1
  99. package/dist/storage/StorageVersionConfig.js +40 -7
  100. package/dist/storage/StorageVersionConfig.js.map +1 -1
  101. package/dist/storage/SyncRulesBucketStorage.d.ts +63 -11
  102. package/dist/storage/SyncRulesBucketStorage.js +14 -0
  103. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  104. package/dist/storage/WriteCheckpointAPI.d.ts +6 -6
  105. package/dist/storage/WriteCheckpointAPI.js +1 -1
  106. package/dist/storage/bson.d.ts +0 -1
  107. package/dist/storage/bson.js +0 -4
  108. package/dist/storage/bson.js.map +1 -1
  109. package/dist/storage/storage-index.d.ts +8 -8
  110. package/dist/storage/storage-index.js +8 -8
  111. package/dist/storage/storage-index.js.map +1 -1
  112. package/dist/storage/storage-metrics.js.map +1 -1
  113. package/dist/streams/streams-index.d.ts +2 -2
  114. package/dist/streams/streams-index.js +2 -2
  115. package/dist/streams/streams-index.js.map +1 -1
  116. package/dist/sync/BucketChecksumState.d.ts +5 -8
  117. package/dist/sync/BucketChecksumState.js +118 -116
  118. package/dist/sync/BucketChecksumState.js.map +1 -1
  119. package/dist/sync/RequestTracker.js +1 -1
  120. package/dist/sync/RequestTracker.js.map +1 -1
  121. package/dist/sync/sync-index.d.ts +2 -2
  122. package/dist/sync/sync-index.js +2 -2
  123. package/dist/sync/sync-index.js.map +1 -1
  124. package/dist/sync/sync.js.map +1 -1
  125. package/dist/sync/util.d.ts +1 -0
  126. package/dist/sync/util.js +10 -0
  127. package/dist/sync/util.js.map +1 -1
  128. package/dist/system/ServiceContext.d.ts +1 -1
  129. package/dist/system/ServiceContext.js +1 -1
  130. package/dist/system/ServiceContext.js.map +1 -1
  131. package/dist/tracing/PerformanceTracer.d.ts +44 -0
  132. package/dist/tracing/PerformanceTracer.js +102 -0
  133. package/dist/tracing/PerformanceTracer.js.map +1 -0
  134. package/dist/tracing/TraceWriter.d.ts +22 -0
  135. package/dist/tracing/TraceWriter.js +63 -0
  136. package/dist/tracing/TraceWriter.js.map +1 -0
  137. package/dist/util/checkpointing.js +1 -1
  138. package/dist/util/config/collectors/impl/base64-config-collector.d.ts +1 -1
  139. package/dist/util/config/collectors/impl/base64-config-collector.js.map +1 -1
  140. package/dist/util/config/collectors/impl/filesystem-config-collector.d.ts +1 -1
  141. package/dist/util/config/collectors/impl/filesystem-config-collector.js +1 -1
  142. package/dist/util/config/collectors/impl/filesystem-config-collector.js.map +1 -1
  143. package/dist/util/config/compound-config-collector.d.ts +1 -1
  144. package/dist/util/config/compound-config-collector.js +2 -2
  145. package/dist/util/config/compound-config-collector.js.map +1 -1
  146. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +1 -1
  147. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
  148. package/dist/util/config/sync-rules/sync-rules-provider.js.map +1 -1
  149. package/dist/util/config.js +1 -1
  150. package/dist/util/config.js.map +1 -1
  151. package/dist/util/env.js +1 -1
  152. package/dist/util/errors.d.ts +3 -0
  153. package/dist/util/errors.js +15 -0
  154. package/dist/util/errors.js.map +1 -0
  155. package/dist/util/protocol-types.d.ts +3 -3
  156. package/dist/util/protocol-types.js +1 -1
  157. package/dist/util/util-index.d.ts +1 -1
  158. package/dist/util/util-index.js +1 -1
  159. package/dist/util/util-index.js.map +1 -1
  160. package/dist/util/utils.d.ts +1 -1
  161. package/package.json +11 -11
  162. package/src/api/RouteAPI.ts +20 -3
  163. package/src/api/api-index.ts +1 -1
  164. package/src/api/api-metrics.ts +1 -1
  165. package/src/api/diagnostics.ts +42 -20
  166. package/src/auth/CachedKeyCollector.ts +2 -3
  167. package/src/auth/CompoundKeyCollector.ts +2 -3
  168. package/src/auth/KeyStore.ts +1 -1
  169. package/src/auth/RemoteJWKSCollector.ts +0 -1
  170. package/src/auth/StaticKeyCollector.ts +1 -1
  171. package/src/auth/StaticSupabaseKeyCollector.ts +1 -1
  172. package/src/auth/utils.ts +5 -2
  173. package/src/entry/commands/compact-action.ts +29 -5
  174. package/src/entry/commands/teardown-action.ts +2 -2
  175. package/src/entry/entry-index.ts +1 -1
  176. package/src/events/EventsEngine.ts +1 -1
  177. package/src/index.ts +2 -0
  178. package/src/metrics/MetricsEngine.ts +1 -1
  179. package/src/metrics/RollingBucketMax.ts +109 -0
  180. package/src/metrics/metrics-index.ts +3 -2
  181. package/src/metrics/open-telemetry/util.ts +1 -1
  182. package/src/metrics/register-metrics.ts +3 -3
  183. package/src/modules/AbstractModule.ts +2 -2
  184. package/src/modules/modules-index.ts +1 -1
  185. package/src/replication/AbstractReplicationJob.ts +3 -3
  186. package/src/replication/AbstractReplicator.ts +32 -30
  187. package/src/replication/ReplicationLagTracker.ts +86 -0
  188. package/src/replication/replication-index.ts +3 -2
  189. package/src/replication/replication-metrics.ts +1 -1
  190. package/src/routes/endpoints/admin.ts +8 -3
  191. package/src/routes/endpoints/checkpointing.ts +1 -1
  192. package/src/routes/endpoints/socket-route.ts +1 -1
  193. package/src/routes/endpoints/sync-rules.ts +10 -12
  194. package/src/routes/endpoints/sync-stream.ts +8 -3
  195. package/src/routes/hooks.ts +2 -2
  196. package/src/routes/route-register.ts +2 -10
  197. package/src/runner/teardown.ts +4 -4
  198. package/src/storage/BucketStorage.ts +9 -9
  199. package/src/storage/BucketStorageBatch.ts +34 -10
  200. package/src/storage/BucketStorageFactory.ts +51 -34
  201. package/src/storage/ChecksumCache.ts +14 -6
  202. package/src/storage/PersistedSyncRulesContent.ts +36 -7
  203. package/src/storage/ReplicationEventPayload.ts +1 -1
  204. package/src/storage/SourceTable.ts +10 -4
  205. package/src/storage/StorageVersionConfig.ts +55 -7
  206. package/src/storage/SyncRulesBucketStorage.ts +77 -11
  207. package/src/storage/WriteCheckpointAPI.ts +6 -6
  208. package/src/storage/bson.ts +0 -5
  209. package/src/storage/storage-index.ts +8 -8
  210. package/src/storage/storage-metrics.ts +2 -2
  211. package/src/streams/streams-index.ts +2 -2
  212. package/src/sync/BucketChecksumState.ts +142 -139
  213. package/src/sync/RequestTracker.ts +1 -1
  214. package/src/sync/sync-index.ts +2 -2
  215. package/src/sync/sync.ts +4 -4
  216. package/src/sync/util.ts +11 -1
  217. package/src/system/ServiceContext.ts +1 -1
  218. package/src/tracing/PerformanceTracer.ts +126 -0
  219. package/src/tracing/TraceWriter.ts +67 -0
  220. package/src/util/checkpointing.ts +1 -1
  221. package/src/util/config/collectors/impl/base64-config-collector.ts +1 -1
  222. package/src/util/config/collectors/impl/filesystem-config-collector.ts +2 -2
  223. package/src/util/config/compound-config-collector.ts +3 -3
  224. package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +1 -1
  225. package/src/util/config/sync-rules/sync-rules-provider.ts +1 -1
  226. package/src/util/config.ts +1 -1
  227. package/src/util/env.ts +1 -1
  228. package/src/util/errors.ts +21 -0
  229. package/src/util/protocol-types.ts +1 -1
  230. package/src/util/util-index.ts +1 -1
  231. package/src/util/utils.ts +1 -1
  232. package/test/src/ReplicationLagTracker.test.ts +53 -0
  233. package/test/src/RollingBucketMax.test.ts +106 -0
  234. package/test/src/auth.test.ts +26 -7
  235. package/test/src/checksum_cache.test.ts +102 -57
  236. package/test/src/config.test.ts +1 -0
  237. package/test/src/diagnostics.test.ts +151 -0
  238. package/test/src/module-loader.test.ts +1 -1
  239. package/test/src/routes/mocks.ts +1 -1
  240. package/test/src/routes/stream.test.ts +1 -2
  241. package/test/src/sync/BucketChecksumState.test.ts +209 -80
  242. package/test/src/util/protocol_types.test.ts +1 -1
  243. package/test/src/utils.ts +9 -0
  244. package/test/tsconfig.json +0 -1
  245. package/tsconfig.tsbuildinfo +1 -1
  246. package/vitest.config.ts +1 -1
@@ -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,15 +15,42 @@ import {
14
15
  } from '@/index.js';
15
16
  import { JSONBig } from '@powersync/service-jsonbig';
16
17
  import {
17
- RequestJwtPayload,
18
+ ParameterIndexLookupCreator,
18
19
  ScopedParameterLookup,
20
+ SourceTableInterface,
19
21
  SqliteJsonRow,
22
+ SqliteRow,
20
23
  SqlSyncRules,
24
+ TablePattern,
21
25
  versionedHydrationState
22
26
  } from '@powersync/service-sync-rules';
27
+ import { ParameterLookupScope } from '@powersync/service-sync-rules/src/HydrationState.js';
23
28
  import { beforeEach, describe, expect, test } from 'vitest';
24
29
 
25
30
  describe('BucketChecksumState', () => {
31
+ const LOOKUP_SOURCE: ParameterIndexLookupCreator = {
32
+ get defaultLookupScope(): ParameterLookupScope {
33
+ return {
34
+ lookupName: 'lookup',
35
+ queryId: '0',
36
+ source: LOOKUP_SOURCE
37
+ };
38
+ },
39
+ getSourceTables(): Set<TablePattern> {
40
+ return new Set();
41
+ },
42
+ evaluateParameterRow(_sourceTable: SourceTableInterface, _row: SqliteRow) {
43
+ return [];
44
+ },
45
+ tableSyncsParameters(_table: SourceTableInterface): boolean {
46
+ return false;
47
+ }
48
+ };
49
+
50
+ function lookupScope(lookupName: string, queryId: string): ParameterLookupScope {
51
+ return { lookupName, queryId, source: LOOKUP_SOURCE };
52
+ }
53
+
26
54
  // Single global[] bucket.
27
55
  // We don't care about data in these tests
28
56
  const SYNC_RULES_GLOBAL = SqlSyncRules.fromYaml(
@@ -67,6 +95,10 @@ bucket_definitions:
67
95
  const syncRequest: StreamingSyncRequest = {};
68
96
  const tokenPayload = new JwtPayload({ sub: '' });
69
97
 
98
+ function bucketStarts(requests: { bucket: string; start: InternalOpId }[]) {
99
+ return new Map(requests.map((request) => [request.bucket, request.start]));
100
+ }
101
+
70
102
  test('global bucket with update', async () => {
71
103
  const storage = new MockBucketChecksumStateStorage();
72
104
  // Set intial state
@@ -94,14 +126,14 @@ bucket_definitions:
94
126
  streams: [{ name: 'global', is_default: true, errors: [] }]
95
127
  }
96
128
  });
97
- expect(line.bucketsToFetch).toEqual([
129
+ expect(line.bucketsToFetch).toMatchObject([
98
130
  {
99
131
  bucket: '1#global[]',
100
132
  priority: 3
101
133
  }
102
134
  ]);
103
135
  // This is the bucket data to be fetched
104
- expect(line.getFilteredBucketPositions()).toEqual(new Map([['1#global[]', 0n]]));
136
+ expect(bucketStarts(line.getFilteredBucketPositions())).toEqual(new Map([['1#global[]', 0n]]));
105
137
 
106
138
  // This similuates the bucket data being sent
107
139
  line.advance();
@@ -132,7 +164,7 @@ bucket_definitions:
132
164
  write_checkpoint: undefined
133
165
  }
134
166
  });
135
- expect(line2.getFilteredBucketPositions()).toEqual(new Map([['1#global[]', 1n]]));
167
+ expect(bucketStarts(line2.getFilteredBucketPositions())).toEqual(new Map([['1#global[]', 1n]]));
136
168
  });
137
169
 
138
170
  test('global bucket with initial state', async () => {
@@ -166,14 +198,14 @@ bucket_definitions:
166
198
  streams: [{ name: 'global', is_default: true, errors: [] }]
167
199
  }
168
200
  });
169
- expect(line.bucketsToFetch).toEqual([
201
+ expect(line.bucketsToFetch).toMatchObject([
170
202
  {
171
203
  bucket: '1#global[]',
172
204
  priority: 3
173
205
  }
174
206
  ]);
175
207
  // This is the main difference between this and the previous test
176
- expect(line.getFilteredBucketPositions()).toEqual(new Map([['1#global[]', 1n]]));
208
+ expect(bucketStarts(line.getFilteredBucketPositions())).toEqual(new Map([['1#global[]', 1n]]));
177
209
  });
178
210
 
179
211
  test('multiple static buckets', async () => {
@@ -206,7 +238,7 @@ bucket_definitions:
206
238
  streams: [{ name: 'global', is_default: true, errors: [] }]
207
239
  }
208
240
  });
209
- expect(line.bucketsToFetch).toEqual([
241
+ expect(line.bucketsToFetch).toMatchObject([
210
242
  {
211
243
  bucket: '2#global[1]',
212
244
  priority: 3
@@ -274,13 +306,13 @@ bucket_definitions:
274
306
  streams: [{ name: 'global', is_default: true, errors: [] }]
275
307
  }
276
308
  });
277
- expect(line.bucketsToFetch).toEqual([
309
+ expect(line.bucketsToFetch).toMatchObject([
278
310
  {
279
311
  bucket: '1#global[]',
280
312
  priority: 3
281
313
  }
282
314
  ]);
283
- expect(line.getFilteredBucketPositions()).toEqual(new Map([['1#global[]', 0n]]));
315
+ expect(bucketStarts(line.getFilteredBucketPositions())).toEqual(new Map([['1#global[]', 0n]]));
284
316
  });
285
317
 
286
318
  test('invalidating individual bucket', async () => {
@@ -337,7 +369,7 @@ bucket_definitions:
337
369
  write_checkpoint: undefined
338
370
  }
339
371
  });
340
- expect(line2.bucketsToFetch).toEqual([{ bucket: '2#global[1]', priority: 3 }]);
372
+ expect(line2.bucketsToFetch).toMatchObject([{ bucket: '2#global[1]', priority: 3 }]);
341
373
  });
342
374
 
343
375
  test('invalidating all buckets', async () => {
@@ -387,7 +419,7 @@ bucket_definitions:
387
419
  write_checkpoint: undefined
388
420
  }
389
421
  });
390
- expect(line2.bucketsToFetch).toEqual([
422
+ expect(line2.bucketsToFetch).toMatchObject([
391
423
  { bucket: '2#global[1]', priority: 3 },
392
424
  { bucket: '2#global[2]', priority: 3 }
393
425
  ]);
@@ -424,7 +456,7 @@ bucket_definitions:
424
456
  streams: [{ name: 'global', is_default: true, errors: [] }]
425
457
  }
426
458
  });
427
- expect(line.bucketsToFetch).toEqual([
459
+ expect(line.bucketsToFetch).toMatchObject([
428
460
  {
429
461
  bucket: '2#global[1]',
430
462
  priority: 3
@@ -436,7 +468,7 @@ bucket_definitions:
436
468
  ]);
437
469
 
438
470
  // This is the bucket data to be fetched
439
- expect(line.getFilteredBucketPositions()).toEqual(
471
+ expect(bucketStarts(line.getFilteredBucketPositions())).toEqual(
440
472
  new Map([
441
473
  ['2#global[1]', 0n],
442
474
  ['2#global[2]', 0n]
@@ -477,7 +509,7 @@ bucket_definitions:
477
509
  }
478
510
  });
479
511
  // This should contain both buckets, even though only one changed.
480
- expect(line2.bucketsToFetch).toEqual([
512
+ expect(line2.bucketsToFetch).toMatchObject([
481
513
  {
482
514
  bucket: '2#global[1]',
483
515
  priority: 3
@@ -488,7 +520,7 @@ bucket_definitions:
488
520
  }
489
521
  ]);
490
522
 
491
- expect(line2.getFilteredBucketPositions()).toEqual(
523
+ expect(bucketStarts(line2.getFilteredBucketPositions())).toEqual(
492
524
  new Map([
493
525
  ['2#global[1]', 3n],
494
526
  ['2#global[2]', 1n]
@@ -513,7 +545,7 @@ bucket_definitions:
513
545
 
514
546
  const line = (await state.buildNextCheckpointLine({
515
547
  base: storage.makeCheckpoint(1n, (lookups) => {
516
- expect(lookups).toEqual([ScopedParameterLookup.direct({ lookupName: 'by_project', queryId: '1' }, ['u1'])]);
548
+ expect(lookups).toEqual([ScopedParameterLookup.direct(lookupScope('by_project', '1'), ['u1'])]);
517
549
  return [{ id: 1 }, { id: 2 }];
518
550
  }),
519
551
  writeCheckpoint: null,
@@ -548,7 +580,7 @@ bucket_definitions:
548
580
  write_checkpoint: undefined
549
581
  }
550
582
  });
551
- expect(line.bucketsToFetch).toEqual([
583
+ expect(line.bucketsToFetch).toMatchObject([
552
584
  {
553
585
  bucket: '3#by_project[1]',
554
586
  priority: 3
@@ -560,7 +592,7 @@ bucket_definitions:
560
592
  ]);
561
593
  line.advance();
562
594
  // This is the bucket data to be fetched
563
- expect(line.getFilteredBucketPositions()).toEqual(
595
+ expect(bucketStarts(line.getFilteredBucketPositions())).toEqual(
564
596
  new Map([
565
597
  ['3#by_project[1]', 0n],
566
598
  ['3#by_project[2]', 0n]
@@ -574,7 +606,7 @@ bucket_definitions:
574
606
  // Now we get a new line
575
607
  const line2 = (await state.buildNextCheckpointLine({
576
608
  base: storage.makeCheckpoint(2n, (lookups) => {
577
- expect(lookups).toEqual([ScopedParameterLookup.direct({ lookupName: 'by_project', queryId: '1' }, ['u1'])]);
609
+ expect(lookups).toEqual([ScopedParameterLookup.direct(lookupScope('by_project', '1'), ['u1'])]);
578
610
  return [{ id: 1 }, { id: 2 }, { id: 3 }];
579
611
  }),
580
612
  writeCheckpoint: null,
@@ -602,7 +634,7 @@ bucket_definitions:
602
634
  write_checkpoint: undefined
603
635
  }
604
636
  });
605
- expect(line2.getFilteredBucketPositions()).toEqual(new Map([['3#by_project[3]', 0n]]));
637
+ expect(bucketStarts(line2.getFilteredBucketPositions())).toEqual(new Map([['3#by_project[3]', 0n]]));
606
638
  });
607
639
 
608
640
  describe('streams', () => {
@@ -869,19 +901,127 @@ config:
869
901
  expect(logData[0].parameter_query_results).toBe(3);
870
902
  });
871
903
 
872
- test('throws error with breakdown when parameter query limit is exceeded', async () => {
904
+ test('throws error with breakdown when too many parameters were fetched', async () => {
905
+ const syncRules = SqlSyncRules.fromYaml(
906
+ `
907
+ config:
908
+ edition: 3
909
+
910
+ streams:
911
+ a:
912
+ auto_subscribe: true
913
+ query: SELECT * FROM a WHERE id IN (SELECT id FROM magic_sequence WHERE count0 = auth.parameter('a'))
914
+ b:
915
+ auto_subscribe: true
916
+ query: SELECT * FROM a WHERE id IN (SELECT id FROM magic_sequence WHERE count1 = auth.parameter('b'))
917
+ c:
918
+ auto_subscribe: true
919
+ query: SELECT * FROM a WHERE id IN (SELECT id FROM magic_sequence WHERE count1 = auth.parameter('c'))
920
+ `,
921
+ { defaultSchema: 'public' }
922
+ ).config.hydrate({
923
+ hydrationState: versionedHydrationState(1)
924
+ });
925
+
926
+ const storage = new MockBucketChecksumStateStorage();
927
+
928
+ const errorMessages: string[] = [];
929
+ const errorData: any[] = [];
930
+ const mockLogger = {
931
+ info: () => {},
932
+ error: (message: string, data: any) => {
933
+ errorMessages.push(message);
934
+ errorData.push(data);
935
+ },
936
+ warn: () => {},
937
+ debug: () => {}
938
+ };
939
+
940
+ const smallContext = new SyncContext({
941
+ maxBuckets: 100,
942
+ maxParameterQueryResults: 55,
943
+ maxDataFetchConcurrency: 10
944
+ });
945
+
946
+ const state = new BucketChecksumState({
947
+ syncContext: smallContext,
948
+ tokenPayload: new JwtPayload({
949
+ sub: 'u1',
950
+ // Create 60 total results: 30 a + 20 b + 10 c
951
+ a: BigInt(30),
952
+ b: BigInt(20),
953
+ c: BigInt(10)
954
+ }),
955
+ syncRequest,
956
+ syncRules,
957
+ bucketStorage: storage,
958
+ logger: mockLogger as any
959
+ });
960
+
961
+ await expect(
962
+ state.buildNextCheckpointLine({
963
+ base: storage.makeCheckpoint(1n, (lookups, limit) => {
964
+ expect(lookups).toHaveLength(1);
965
+ const [{ values }] = lookups;
966
+ expect(values[0]).toStrictEqual('lookup');
967
+
968
+ const count = Number(values[2]); // The count parameter from streams
969
+ if (count > limit) {
970
+ throw new ParameterSetLimitExceededError(limit);
971
+ }
972
+ return Array.from({ length: count }, (_, i) => ({ '0': BigInt(i) }));
973
+ }),
974
+ writeCheckpoint: null,
975
+ update: CHECKPOINT_INVALIDATE_ALL
976
+ })
977
+ ).rejects.toThrow('Too many parameter query results (limit of 55)');
978
+
979
+ // Verify error log includes breakdown
980
+ expect(errorMessages[0]).toContain('Invoked parameter queries by definition:');
981
+ expect(errorMessages[0]).toContain('Stream a evaluating parameter on magic_sequence: 30 results.');
982
+ expect(errorMessages[0]).toContain('Stream b evaluating parameter on magic_sequence: 20 results.');
983
+ expect(errorMessages[0]).toContain(
984
+ 'Stream c evaluating parameter on magic_sequence exceeded the remaining limit of 5 available results.'
985
+ );
986
+
987
+ expect(errorData[0].checkpoint).toEqual(1n);
988
+ expect(errorData[0].parameterResults).toEqual([
989
+ {
990
+ definition: 'Stream a evaluating parameter on magic_sequence',
991
+ didExceedLimit: false,
992
+ resultsOrLimit: 30
993
+ },
994
+ {
995
+ definition: 'Stream b evaluating parameter on magic_sequence',
996
+ didExceedLimit: false,
997
+ resultsOrLimit: 20
998
+ },
999
+ {
1000
+ definition: 'Stream c evaluating parameter on magic_sequence',
1001
+ didExceedLimit: true,
1002
+ resultsOrLimit: 5
1003
+ }
1004
+ ]);
1005
+ });
1006
+
1007
+ test('throws error with breakdown when bucket limit is exceeded', async () => {
1008
+ // These streams are designed to return buckets without consuming too many parameters (as exceeding that limit is
1009
+ // a different error).
873
1010
  const SYNC_RULES_MULTI = SqlSyncRules.fromYaml(
874
1011
  `
875
- bucket_definitions:
1012
+ config:
1013
+ edition: 3
1014
+
1015
+ streams:
876
1016
  projects:
877
- parameters: select id from projects where user_id = request.user_id()
878
- data: []
1017
+ auto_subscribe: true
1018
+ query: SELECT id FROM projects WHERE p IN auth.parameter('a')
879
1019
  tasks:
880
- parameters: select id from tasks where user_id = request.user_id()
881
- data: []
1020
+ auto_subscribe: true
1021
+ query: SELECT id FROM tasks WHERE p IN auth.parameter('b')
882
1022
  comments:
883
- parameters: select id from comments where user_id = request.user_id()
884
- data: []
1023
+ auto_subscribe: true
1024
+ query: SELECT id FROM comments WHERE p IN auth.parameter('c')
885
1025
  `,
886
1026
  { defaultSchema: 'public' }
887
1027
  ).config.hydrate({ hydrationState: versionedHydrationState(4) });
@@ -901,74 +1041,66 @@ bucket_definitions:
901
1041
  };
902
1042
 
903
1043
  const smallContext = new SyncContext({
904
- maxBuckets: 100,
905
- maxParameterQueryResults: 50,
1044
+ maxBuckets: 50,
1045
+ maxParameterQueryResults: 10,
906
1046
  maxDataFetchConcurrency: 10
907
1047
  });
908
1048
 
909
1049
  const state = new BucketChecksumState({
910
1050
  syncContext: smallContext,
911
- tokenPayload: new JwtPayload({ sub: 'u1' }),
1051
+ tokenPayload: new JwtPayload({
1052
+ sub: 'u1',
1053
+ // Create 60 total results: 30 projects + 20 tasks + 10 comments
1054
+ a: Array.from({ length: 30 }, (_, i) => BigInt(i)),
1055
+ b: Array.from({ length: 20 }, (_, i) => BigInt(i)),
1056
+ c: Array.from({ length: 10 }, (_, i) => BigInt(i))
1057
+ }),
912
1058
  syncRequest,
913
1059
  syncRules: SYNC_RULES_MULTI,
914
1060
  bucketStorage: storage,
915
1061
  logger: mockLogger as any
916
1062
  });
917
1063
 
918
- // Create 60 total results: 30 projects + 20 tasks + 10 comments
919
- const projectIds = Array.from({ length: 30 }, (_, i) => ({ id: i + 1 }));
920
- const taskIds = Array.from({ length: 20 }, (_, i) => ({ id: i + 1 }));
921
- const commentIds = Array.from({ length: 10 }, (_, i) => ({ id: i + 1 }));
922
-
923
- for (let i = 1; i <= 30; i++) {
924
- storage.updateTestChecksum({ bucket: `projects[${i}]`, checksum: 1, count: 1 });
925
- }
926
- for (let i = 1; i <= 20; i++) {
927
- storage.updateTestChecksum({ bucket: `tasks[${i}]`, checksum: 1, count: 1 });
928
- }
929
- for (let i = 1; i <= 10; i++) {
930
- storage.updateTestChecksum({ bucket: `comments[${i}]`, checksum: 1, count: 1 });
931
- }
932
-
1064
+ let invokedParameterQueries = 0;
933
1065
  await expect(
934
1066
  state.buildNextCheckpointLine({
935
- base: storage.makeCheckpoint(1n, (lookups) => {
936
- const lookup = lookups[0];
937
- const lookupName = lookup.values[0];
938
- if (lookupName === 'projects') {
939
- return projectIds;
940
- } else if (lookupName === 'tasks') {
941
- return taskIds;
942
- } else {
943
- return commentIds;
944
- }
1067
+ base: storage.makeCheckpoint(1n, () => {
1068
+ invokedParameterQueries++;
1069
+ return [{ '0': 'user' }];
945
1070
  }),
946
1071
  writeCheckpoint: null,
947
1072
  update: CHECKPOINT_INVALIDATE_ALL
948
1073
  })
949
- ).rejects.toThrow('Too many parameter query results: 60 (limit of 50)');
1074
+ ).rejects.toThrow('Too many buckets: 60 (limit of 50');
1075
+ expect(invokedParameterQueries).toStrictEqual(0);
950
1076
 
951
1077
  // Verify error log includes breakdown
952
- expect(errorMessages[0]).toContain('Parameter query results by definition:');
1078
+ expect(errorMessages[0]).toContain('Buckets by definition:');
953
1079
  expect(errorMessages[0]).toContain('projects: 30');
954
1080
  expect(errorMessages[0]).toContain('tasks: 20');
955
1081
  expect(errorMessages[0]).toContain('comments: 10');
956
1082
 
957
- expect(errorData[0].parameter_query_results).toBe(60);
958
- expect(errorData[0].parameter_query_results_by_definition).toEqual({
1083
+ expect(errorData[0].checkpoint).toEqual(1n);
1084
+ expect(errorData[0].buckets).toBe(60);
1085
+ expect(errorData[0].buckets_by_definition).toEqual({
959
1086
  projects: 30,
960
1087
  tasks: 20,
961
1088
  comments: 10
962
1089
  });
963
1090
  });
964
1091
 
965
- test('limits breakdown to top 10 definitions', async () => {
966
- // Create sync rules with 15 different definitions with dynamic parameters
967
- let yamlDefinitions = 'bucket_definitions:\n';
1092
+ test('limits bucket breakdown to top 10 definitions', async () => {
1093
+ // Create sync streams with 15 different definitions with dynamic parameters
1094
+ let yamlDefinitions = `
1095
+ config:
1096
+ edition: 3
1097
+
1098
+ streams:
1099
+ `;
968
1100
  for (let i = 1; i <= 15; i++) {
969
1101
  yamlDefinitions += ` def${i}:\n`;
970
- yamlDefinitions += ` parameters: select id from def${i}_table where user_id = request.user_id()\n`;
971
- yamlDefinitions += ` data: []\n`;
1102
+ yamlDefinitions += ` auto_subscribe: true\n`;
1103
+ yamlDefinitions += ` query: SELECT * FROM tbl WHERE b = auth.parameter('${i}')\n`;
972
1104
  }
973
1105
 
974
1106
  const SYNC_RULES_MANY = SqlSyncRules.fromYaml(yamlDefinitions, { defaultSchema: 'public' }).config.hydrate({
@@ -988,35 +1120,32 @@ bucket_definitions:
988
1120
  };
989
1121
 
990
1122
  const smallContext = new SyncContext({
991
- maxBuckets: 100,
1123
+ maxBuckets: 10,
992
1124
  maxParameterQueryResults: 10,
993
1125
  maxDataFetchConcurrency: 10
994
1126
  });
995
1127
 
996
1128
  const state = new BucketChecksumState({
997
1129
  syncContext: smallContext,
998
- tokenPayload: new JwtPayload({ sub: 'u1' }),
1130
+ tokenPayload: new JwtPayload({
1131
+ sub: 'u1',
1132
+ ...Object.fromEntries(Array.from({ length: 30 }, (_, i) => [i.toString(), BigInt(i)]))
1133
+ }),
999
1134
  syncRequest,
1000
1135
  syncRules: SYNC_RULES_MANY,
1001
1136
  bucketStorage: storage,
1002
1137
  logger: mockLogger as any
1003
1138
  });
1004
1139
 
1005
- // Each definition creates one bucket, total 15 buckets
1006
- for (let i = 1; i <= 15; i++) {
1007
- storage.updateTestChecksum({ bucket: `def${i}[${i}]`, checksum: 1, count: 1 });
1008
- }
1009
-
1010
1140
  await expect(
1011
1141
  state.buildNextCheckpointLine({
1012
1142
  base: storage.makeCheckpoint(1n, (lookups) => {
1013
- // Return one result for each definition
1014
- return [{ id: 1 }];
1143
+ return [{ '0': 'user' }];
1015
1144
  }),
1016
1145
  writeCheckpoint: null,
1017
1146
  update: CHECKPOINT_INVALIDATE_ALL
1018
1147
  })
1019
- ).rejects.toThrow('Too many parameter query results: 15 (limit of 10)');
1148
+ ).rejects.toThrow('Too many buckets: 15 (limit of 10)');
1020
1149
 
1021
1150
  // Verify only top 10 are shown
1022
1151
  const errorMessage = errorMessages[0];
@@ -1044,9 +1173,9 @@ class MockBucketChecksumStateStorage implements BucketChecksumStateStorage {
1044
1173
  this.filter?.({ invalidate: true });
1045
1174
  }
1046
1175
 
1047
- async getChecksums(checkpoint: InternalOpId, buckets: string[]): Promise<ChecksumMap> {
1176
+ async getChecksums(_checkpoint: InternalOpId, buckets: { bucket: string }[]): Promise<ChecksumMap> {
1048
1177
  return new Map<string, BucketChecksum>(
1049
- buckets.map((bucket) => {
1178
+ buckets.map(({ bucket }) => {
1050
1179
  const checksum = this.state.get(bucket);
1051
1180
  return [
1052
1181
  bucket,
@@ -1062,16 +1191,16 @@ class MockBucketChecksumStateStorage implements BucketChecksumStateStorage {
1062
1191
 
1063
1192
  makeCheckpoint(
1064
1193
  opId: InternalOpId,
1065
- parameters?: (lookups: ScopedParameterLookup[]) => SqliteJsonRow[]
1194
+ parameters?: (lookups: ScopedParameterLookup[], limit: number) => SqliteJsonRow[]
1066
1195
  ): ReplicationCheckpoint {
1067
1196
  return {
1068
1197
  checkpoint: opId,
1069
1198
  lsn: String(opId),
1070
- getParameterSets: async (lookups: ScopedParameterLookup[]) => {
1199
+ getParameterSets: async (lookups: ScopedParameterLookup[], limit) => {
1071
1200
  if (parameters == null) {
1072
1201
  throw new Error(`getParametersSets not defined for checkpoint ${opId}`);
1073
1202
  }
1074
- return parameters(lookups);
1203
+ return parameters(lookups, limit);
1075
1204
  }
1076
1205
  };
1077
1206
  }
@@ -1,6 +1,6 @@
1
1
  import { StreamingSyncRequest } from '@/index.js';
2
2
  import { schema } from '@powersync/lib-services-framework';
3
- import { describe, test, expect, it } from 'vitest';
3
+ import { describe, expect, test } from 'vitest';
4
4
 
5
5
  describe('protocol types', () => {
6
6
  describe('StreamingSyncRequest', () => {
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Removes the source property from an object.
3
+ *
4
+ * This is for tests where we don't care about this value, and it adds a lot of noise in the output.
5
+ */
6
+ export function removeSource<T extends { source?: any }>(obj: T): Omit<T, 'source'> {
7
+ const { source, ...rest } = obj;
8
+ return rest;
9
+ }
@@ -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/*"]