@powersync/service-core 1.20.4 → 1.21.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 (220) hide show
  1. package/CHANGELOG.md +53 -0
  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/entry/commands/teardown-action.js +2 -2
  20. package/dist/entry/commands/teardown-action.js.map +1 -1
  21. package/dist/entry/entry-index.d.ts +1 -1
  22. package/dist/entry/entry-index.js +1 -1
  23. package/dist/entry/entry-index.js.map +1 -1
  24. package/dist/events/EventsEngine.js +1 -1
  25. package/dist/events/EventsEngine.js.map +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +1 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/metrics/MetricsEngine.d.ts +1 -1
  30. package/dist/metrics/metrics-index.d.ts +3 -3
  31. package/dist/metrics/metrics-index.js +3 -3
  32. package/dist/metrics/metrics-index.js.map +1 -1
  33. package/dist/metrics/open-telemetry/util.js +1 -1
  34. package/dist/metrics/open-telemetry/util.js.map +1 -1
  35. package/dist/metrics/register-metrics.js +2 -2
  36. package/dist/metrics/register-metrics.js.map +1 -1
  37. package/dist/modules/AbstractModule.d.ts +2 -2
  38. package/dist/modules/AbstractModule.js.map +1 -1
  39. package/dist/modules/modules-index.d.ts +1 -1
  40. package/dist/modules/modules-index.js +1 -1
  41. package/dist/modules/modules-index.js.map +1 -1
  42. package/dist/replication/AbstractReplicationJob.d.ts +1 -1
  43. package/dist/replication/AbstractReplicationJob.js +1 -1
  44. package/dist/replication/AbstractReplicationJob.js.map +1 -1
  45. package/dist/replication/AbstractReplicator.d.ts +6 -6
  46. package/dist/replication/AbstractReplicator.js +21 -21
  47. package/dist/replication/AbstractReplicator.js.map +1 -1
  48. package/dist/replication/replication-index.d.ts +3 -3
  49. package/dist/replication/replication-index.js +3 -3
  50. package/dist/replication/replication-index.js.map +1 -1
  51. package/dist/replication/replication-metrics.js.map +1 -1
  52. package/dist/routes/configure-fastify.d.ts +59 -32
  53. package/dist/routes/endpoints/admin.d.ts +108 -54
  54. package/dist/routes/endpoints/admin.js +7 -3
  55. package/dist/routes/endpoints/admin.js.map +1 -1
  56. package/dist/routes/endpoints/checkpointing.js +1 -1
  57. package/dist/routes/endpoints/checkpointing.js.map +1 -1
  58. package/dist/routes/endpoints/socket-route.js +1 -1
  59. package/dist/routes/endpoints/socket-route.js.map +1 -1
  60. package/dist/routes/endpoints/sync-rules.js +10 -10
  61. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  62. package/dist/routes/endpoints/sync-stream.d.ts +10 -10
  63. package/dist/routes/endpoints/sync-stream.js +2 -2
  64. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  65. package/dist/routes/hooks.js +1 -1
  66. package/dist/routes/hooks.js.map +1 -1
  67. package/dist/routes/route-register.js.map +1 -1
  68. package/dist/runner/teardown.js +4 -4
  69. package/dist/runner/teardown.js.map +1 -1
  70. package/dist/storage/BucketStorage.d.ts +9 -9
  71. package/dist/storage/BucketStorage.js +9 -9
  72. package/dist/storage/BucketStorageBatch.d.ts +1 -1
  73. package/dist/storage/BucketStorageFactory.d.ts +27 -20
  74. package/dist/storage/BucketStorageFactory.js +19 -16
  75. package/dist/storage/BucketStorageFactory.js.map +1 -1
  76. package/dist/storage/ChecksumCache.js.map +1 -1
  77. package/dist/storage/PersistedSyncRulesContent.d.ts +3 -1
  78. package/dist/storage/PersistedSyncRulesContent.js +24 -5
  79. package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
  80. package/dist/storage/ReplicationEventPayload.d.ts +1 -1
  81. package/dist/storage/SourceTable.d.ts +4 -4
  82. package/dist/storage/SourceTable.js +3 -3
  83. package/dist/storage/SourceTable.js.map +1 -1
  84. package/dist/storage/StorageVersionConfig.d.ts +1 -1
  85. package/dist/storage/StorageVersionConfig.js +1 -1
  86. package/dist/storage/SyncRulesBucketStorage.d.ts +38 -6
  87. package/dist/storage/SyncRulesBucketStorage.js +14 -0
  88. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  89. package/dist/storage/WriteCheckpointAPI.d.ts +6 -6
  90. package/dist/storage/WriteCheckpointAPI.js +1 -1
  91. package/dist/storage/bson.d.ts +0 -1
  92. package/dist/storage/bson.js +0 -4
  93. package/dist/storage/bson.js.map +1 -1
  94. package/dist/storage/storage-index.d.ts +8 -8
  95. package/dist/storage/storage-index.js +8 -8
  96. package/dist/storage/storage-index.js.map +1 -1
  97. package/dist/storage/storage-metrics.js.map +1 -1
  98. package/dist/streams/streams-index.d.ts +2 -2
  99. package/dist/streams/streams-index.js +2 -2
  100. package/dist/streams/streams-index.js.map +1 -1
  101. package/dist/sync/BucketChecksumState.d.ts +2 -5
  102. package/dist/sync/BucketChecksumState.js +119 -75
  103. package/dist/sync/BucketChecksumState.js.map +1 -1
  104. package/dist/sync/RequestTracker.js +1 -1
  105. package/dist/sync/RequestTracker.js.map +1 -1
  106. package/dist/sync/sync-index.d.ts +2 -2
  107. package/dist/sync/sync-index.js +2 -2
  108. package/dist/sync/sync-index.js.map +1 -1
  109. package/dist/sync/sync.js.map +1 -1
  110. package/dist/sync/util.js.map +1 -1
  111. package/dist/system/ServiceContext.d.ts +1 -1
  112. package/dist/system/ServiceContext.js +1 -1
  113. package/dist/system/ServiceContext.js.map +1 -1
  114. package/dist/tracing/PerformanceTracer.d.ts +44 -0
  115. package/dist/tracing/PerformanceTracer.js +102 -0
  116. package/dist/tracing/PerformanceTracer.js.map +1 -0
  117. package/dist/tracing/TraceWriter.d.ts +22 -0
  118. package/dist/tracing/TraceWriter.js +63 -0
  119. package/dist/tracing/TraceWriter.js.map +1 -0
  120. package/dist/util/checkpointing.js +1 -1
  121. package/dist/util/config/collectors/impl/base64-config-collector.d.ts +1 -1
  122. package/dist/util/config/collectors/impl/base64-config-collector.js.map +1 -1
  123. package/dist/util/config/collectors/impl/filesystem-config-collector.d.ts +1 -1
  124. package/dist/util/config/collectors/impl/filesystem-config-collector.js +1 -1
  125. package/dist/util/config/collectors/impl/filesystem-config-collector.js.map +1 -1
  126. package/dist/util/config/compound-config-collector.d.ts +1 -1
  127. package/dist/util/config/compound-config-collector.js +2 -2
  128. package/dist/util/config/compound-config-collector.js.map +1 -1
  129. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +1 -1
  130. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
  131. package/dist/util/config/sync-rules/sync-rules-provider.js.map +1 -1
  132. package/dist/util/config.js +1 -1
  133. package/dist/util/config.js.map +1 -1
  134. package/dist/util/env.js +1 -1
  135. package/dist/util/errors.d.ts +3 -0
  136. package/dist/util/errors.js +15 -0
  137. package/dist/util/errors.js.map +1 -0
  138. package/dist/util/protocol-types.d.ts +3 -3
  139. package/dist/util/protocol-types.js +1 -1
  140. package/dist/util/util-index.d.ts +1 -1
  141. package/dist/util/util-index.js +1 -1
  142. package/dist/util/util-index.js.map +1 -1
  143. package/dist/util/utils.d.ts +1 -1
  144. package/package.json +11 -11
  145. package/src/api/RouteAPI.ts +20 -3
  146. package/src/api/api-index.ts +1 -1
  147. package/src/api/api-metrics.ts +1 -1
  148. package/src/api/diagnostics.ts +42 -20
  149. package/src/auth/CachedKeyCollector.ts +2 -3
  150. package/src/auth/CompoundKeyCollector.ts +2 -3
  151. package/src/auth/KeyStore.ts +1 -1
  152. package/src/auth/RemoteJWKSCollector.ts +0 -1
  153. package/src/auth/StaticKeyCollector.ts +1 -1
  154. package/src/auth/StaticSupabaseKeyCollector.ts +1 -1
  155. package/src/entry/commands/teardown-action.ts +2 -2
  156. package/src/entry/entry-index.ts +1 -1
  157. package/src/events/EventsEngine.ts +1 -1
  158. package/src/index.ts +2 -0
  159. package/src/metrics/MetricsEngine.ts +1 -1
  160. package/src/metrics/metrics-index.ts +3 -3
  161. package/src/metrics/open-telemetry/util.ts +1 -1
  162. package/src/metrics/register-metrics.ts +3 -3
  163. package/src/modules/AbstractModule.ts +2 -2
  164. package/src/modules/modules-index.ts +1 -1
  165. package/src/replication/AbstractReplicationJob.ts +2 -2
  166. package/src/replication/AbstractReplicator.ts +23 -23
  167. package/src/replication/replication-index.ts +3 -3
  168. package/src/replication/replication-metrics.ts +1 -1
  169. package/src/routes/endpoints/admin.ts +7 -3
  170. package/src/routes/endpoints/checkpointing.ts +1 -1
  171. package/src/routes/endpoints/socket-route.ts +1 -1
  172. package/src/routes/endpoints/sync-rules.ts +10 -12
  173. package/src/routes/endpoints/sync-stream.ts +2 -2
  174. package/src/routes/hooks.ts +2 -2
  175. package/src/routes/route-register.ts +2 -10
  176. package/src/runner/teardown.ts +4 -4
  177. package/src/storage/BucketStorage.ts +9 -9
  178. package/src/storage/BucketStorageBatch.ts +1 -1
  179. package/src/storage/BucketStorageFactory.ts +45 -34
  180. package/src/storage/ChecksumCache.ts +1 -1
  181. package/src/storage/PersistedSyncRulesContent.ts +30 -6
  182. package/src/storage/ReplicationEventPayload.ts +1 -1
  183. package/src/storage/SourceTable.ts +4 -4
  184. package/src/storage/StorageVersionConfig.ts +1 -1
  185. package/src/storage/SyncRulesBucketStorage.ts +46 -7
  186. package/src/storage/WriteCheckpointAPI.ts +6 -6
  187. package/src/storage/bson.ts +0 -5
  188. package/src/storage/storage-index.ts +8 -8
  189. package/src/storage/storage-metrics.ts +2 -2
  190. package/src/streams/streams-index.ts +2 -2
  191. package/src/sync/BucketChecksumState.ts +141 -93
  192. package/src/sync/RequestTracker.ts +1 -1
  193. package/src/sync/sync-index.ts +2 -2
  194. package/src/sync/sync.ts +2 -8
  195. package/src/sync/util.ts +1 -1
  196. package/src/system/ServiceContext.ts +1 -1
  197. package/src/tracing/PerformanceTracer.ts +126 -0
  198. package/src/tracing/TraceWriter.ts +67 -0
  199. package/src/util/checkpointing.ts +1 -1
  200. package/src/util/config/collectors/impl/base64-config-collector.ts +1 -1
  201. package/src/util/config/collectors/impl/filesystem-config-collector.ts +2 -2
  202. package/src/util/config/compound-config-collector.ts +3 -3
  203. package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +1 -1
  204. package/src/util/config/sync-rules/sync-rules-provider.ts +1 -1
  205. package/src/util/config.ts +1 -1
  206. package/src/util/env.ts +1 -1
  207. package/src/util/errors.ts +21 -0
  208. package/src/util/protocol-types.ts +1 -1
  209. package/src/util/util-index.ts +1 -1
  210. package/src/util/utils.ts +1 -1
  211. package/test/src/auth.test.ts +115 -7
  212. package/test/src/diagnostics.test.ts +151 -0
  213. package/test/src/module-loader.test.ts +1 -1
  214. package/test/src/routes/mocks.ts +1 -1
  215. package/test/src/routes/stream.test.ts +1 -2
  216. package/test/src/sync/BucketChecksumState.test.ts +223 -67
  217. package/test/src/util/protocol_types.test.ts +1 -1
  218. package/test/tsconfig.json +0 -1
  219. package/tsconfig.tsbuildinfo +1 -1
  220. package/vitest.config.ts +1 -1
@@ -2,9 +2,10 @@ import { Logger, ObserverClient } from '@powersync/lib-services-framework';
2
2
  import {
3
3
  BucketDataSource,
4
4
  HydratedSyncRules,
5
- ScopedParameterLookup,
6
- SqliteJsonRow
5
+ ParameterLookupRows,
6
+ ScopedParameterLookup
7
7
  } from '@powersync/service-sync-rules';
8
+ import { PerformanceTracer } from '../tracing/PerformanceTracer.js';
8
9
  import * as util from '../util/util-index.js';
9
10
  import { BucketStorageBatch, FlushedResult, SaveUpdate } from './BucketStorageBatch.js';
10
11
  import { BucketStorageFactory } from './BucketStorageFactory.js';
@@ -14,7 +15,7 @@ import { SourceTable } from './SourceTable.js';
14
15
  import { SyncStorageWriteCheckpointAPI } from './WriteCheckpointAPI.js';
15
16
 
16
17
  /**
17
- * Storage for a specific copy of sync rules.
18
+ * Storage for a specific replication stream.
18
19
  */
19
20
  export interface SyncRulesBucketStorage
20
21
  extends ObserverClient<SyncRulesBucketStorageListener>,
@@ -23,6 +24,7 @@ export interface SyncRulesBucketStorage
23
24
  readonly slot_name: string;
24
25
 
25
26
  readonly factory: BucketStorageFactory;
27
+ readonly logger: Logger;
26
28
 
27
29
  /**
28
30
  * Resolve a table, keeping track of it internally.
@@ -47,11 +49,11 @@ export interface SyncRulesBucketStorage
47
49
  getParsedSyncRules(options: ParseSyncRulesOptions): HydratedSyncRules;
48
50
 
49
51
  /**
50
- * Terminate the sync rules.
52
+ * Terminate the replication stream.
51
53
  *
52
54
  * This clears the storage, and sets state to TERMINATED.
53
55
  *
54
- * Must only be called on stopped sync rules.
56
+ * Must only be called on stopped replication streams.
55
57
  */
56
58
  terminate(options?: TerminateOptions): Promise<void>;
57
59
 
@@ -98,7 +100,7 @@ export interface SyncRulesBucketStorage
98
100
  /**
99
101
  * Yields the latest user write checkpoint whenever the sync checkpoint updates.
100
102
  *
101
- * The stream stops or errors if this is not the active sync rules (anymore).
103
+ * The stream stops or errors if this is not the active sync config (anymore).
102
104
  */
103
105
  watchCheckpointChanges(options: WatchWriteCheckpointOptions): AsyncIterable<StorageCheckpointUpdate>;
104
106
 
@@ -196,6 +198,8 @@ export interface CreateWriterOptions extends ParseSyncRulesOptions {
196
198
  */
197
199
  markRecordUnavailable?: BucketStorageMarkRecordUnavailable;
198
200
 
201
+ tracer?: PerformanceTracer<'storage' | 'evaluate'>;
202
+
199
203
  logger?: Logger;
200
204
  }
201
205
 
@@ -265,6 +269,8 @@ export interface CompactOptions {
265
269
  compactParameterCacheLimit?: number;
266
270
 
267
271
  signal?: AbortSignal;
272
+
273
+ logger?: Logger;
268
274
  }
269
275
 
270
276
  export interface PopulateChecksumCacheOptions {
@@ -320,8 +326,41 @@ export interface ReplicationCheckpoint {
320
326
  * Used to resolve "dynamic" parameter queries.
321
327
  *
322
328
  * This gets parameter sets specific to this checkpoint.
329
+ *
330
+ * @throws {@link ParameterSetLimitExceededError}
331
+ * Thrown if resolved lookups in bucket storage exceed the `limit` parameter.
332
+ */
333
+ getParameterSets(lookups: ScopedParameterLookup[], limit: number): Promise<ParameterLookupRows[]>;
334
+ }
335
+
336
+ /**
337
+ * An exception thrown by {@link ReplicationCheckpoint} implementations if there are too many parameter results.
338
+ *
339
+ * This is not a suitable exception to show to users, `BucketParameterState` adds additional context.
340
+ */
341
+ export class ParameterSetLimitExceededError extends Error {
342
+ constructor(
343
+ readonly limit: number,
344
+ readonly breakdown?: ParameterQueryInvocationLog[]
345
+ ) {
346
+ super(`Too many parameter results (limit was ${limit})`);
347
+ }
348
+ }
349
+
350
+ export interface ParameterQueryInvocationLog {
351
+ /**
352
+ * The definition for which a parameter query was invoked.
353
+ *
354
+ * The exact format of definition is unspecified, it's shown to users to help them debug this failure.
355
+ */
356
+ definition: string;
357
+ /**
358
+ * If {@link didExceedLimit} is false, the amount of rows returned by the invocation.
359
+ *
360
+ * Otherwise, the maximum amount of rows this invocation was allowed to return.
323
361
  */
324
- getParameterSets(lookups: ScopedParameterLookup[]): Promise<SqliteJsonRow[]>;
362
+ resultsOrLimit: number;
363
+ didExceedLimit: boolean;
325
364
  }
326
365
 
327
366
  export interface WatchWriteCheckpointOptions {
@@ -1,7 +1,7 @@
1
1
  export enum WriteCheckpointMode {
2
2
  /**
3
3
  * Raw mappings of `user_id` to `write_checkpoint`s should
4
- * be supplied for each set of sync rules.
4
+ * be supplied for each replication stream.
5
5
  */
6
6
  CUSTOM = 'custom',
7
7
  /**
@@ -21,7 +21,7 @@ export interface BaseWriteCheckpointIdentifier {
21
21
 
22
22
  export interface CustomWriteCheckpointFilters extends BaseWriteCheckpointIdentifier {
23
23
  /**
24
- * Sync rules which were active when this checkpoint was created.
24
+ * Replication stream which was active when this checkpoint was created.
25
25
  */
26
26
  sync_rules_id: number;
27
27
  }
@@ -35,7 +35,7 @@ export interface BatchedCustomWriteCheckpointOptions extends BaseWriteCheckpoint
35
35
 
36
36
  export interface CustomWriteCheckpointOptions extends BatchedCustomWriteCheckpointOptions {
37
37
  /**
38
- * Sync rules which were active when this checkpoint was created.
38
+ * Replication stream which was active when this checkpoint was created.
39
39
  */
40
40
  sync_rules_id: number;
41
41
  }
@@ -63,8 +63,8 @@ export interface BaseWriteCheckpointAPI {
63
63
 
64
64
  /**
65
65
  * Write Checkpoint API to be used in conjunction with a {@link SyncRulesBucketStorage}.
66
- * This storage corresponds with a set of sync rules. These APIs don't require specifying a
67
- * sync rules id.
66
+ * This storage corresponds with a replication stream. These APIs don't require specifying a
67
+ * replication stream id.
68
68
  */
69
69
  export interface SyncStorageWriteCheckpointAPI extends BaseWriteCheckpointAPI {
70
70
  lastWriteCheckpoint(filters: SyncStorageLastWriteCheckpointFilters): Promise<bigint | null>;
@@ -72,7 +72,7 @@ export interface SyncStorageWriteCheckpointAPI extends BaseWriteCheckpointAPI {
72
72
 
73
73
  /**
74
74
  * Write Checkpoint API which is interfaced directly with the storage layer. This requires
75
- * sync rules identifiers for custom write checkpoints.
75
+ * replication stream identifiers for custom write checkpoints.
76
76
  */
77
77
  export interface WriteCheckpointAPI extends BaseWriteCheckpointAPI {
78
78
  lastWriteCheckpoint(filters: LastWriteCheckpointFilters): Promise<bigint | null>;
@@ -40,11 +40,6 @@ export const deserializeParameterLookup = (lookup: bson.Binary) => {
40
40
  return parsed;
41
41
  };
42
42
 
43
- export const getLookupBucketDefinitionName = (lookup: bson.Binary) => {
44
- const parsed = deserializeParameterLookup(lookup);
45
- return parsed[0] as string;
46
- };
47
-
48
43
  /**
49
44
  * True if this is a bson.UUID.
50
45
  *
@@ -1,17 +1,17 @@
1
1
  export * from './bson.js';
2
2
  export * from './BucketStorage.js';
3
+ export * from './BucketStorageBatch.js';
4
+ export * from './BucketStorageFactory.js';
3
5
  export * from './ChecksumCache.js';
6
+ export * from './PersistedSyncRulesContent.js';
4
7
  export * from './ReplicationEventPayload.js';
8
+ export * from './ReplicationLock.js';
9
+ export * from './ReportStorage.js';
5
10
  export * from './SourceEntity.js';
6
11
  export * from './SourceTable.js';
12
+ export * from './storage-metrics.js';
7
13
  export * from './StorageEngine.js';
8
14
  export * from './StorageProvider.js';
9
- export * from './storage-metrics.js';
10
- export * from './WriteCheckpointAPI.js';
11
- export * from './BucketStorageFactory.js';
12
- export * from './BucketStorageBatch.js';
13
- export * from './SyncRulesBucketStorage.js';
14
- export * from './PersistedSyncRulesContent.js';
15
- export * from './ReplicationLock.js';
16
- export * from './ReportStorage.js';
17
15
  export * from './StorageVersionConfig.js';
16
+ export * from './SyncRulesBucketStorage.js';
17
+ export * from './WriteCheckpointAPI.js';
@@ -1,7 +1,7 @@
1
- import { MetricsEngine } from '../metrics/MetricsEngine.js';
2
1
  import { logger } from '@powersync/lib-services-framework';
3
- import { BucketStorageFactory, StorageMetrics } from './BucketStorageFactory.js';
4
2
  import { StorageMetric } from '@powersync/service-types';
3
+ import { MetricsEngine } from '../metrics/MetricsEngine.js';
4
+ import { BucketStorageFactory, StorageMetrics } from './BucketStorageFactory.js';
5
5
 
6
6
  export function createCoreStorageMetrics(engine: MetricsEngine): void {
7
7
  engine.createObservableGauge({
@@ -1,3 +1,3 @@
1
- export * from './merge.js';
2
- export * from './LastValueSink.js';
3
1
  export * from './BroadcastIterable.js';
2
+ export * from './LastValueSink.js';
3
+ export * from './merge.js';
@@ -1,14 +1,13 @@
1
1
  import {
2
- BucketDescription,
3
2
  BucketParameterQuerier,
4
3
  BucketPriority,
5
4
  BucketSource,
6
5
  HydratedSyncRules,
6
+ mergeBuckets,
7
7
  QuerierError,
8
8
  RequestedStream,
9
9
  RequestParameters,
10
- ResolvedBucket,
11
- mergeBuckets
10
+ ResolvedBucket
12
11
  } from '@powersync/service-sync-rules';
13
12
 
14
13
  import * as storage from '../storage/storage-index.js';
@@ -22,6 +21,7 @@ import {
22
21
  ServiceError
23
22
  } from '@powersync/lib-services-framework';
24
23
  import { JwtPayload } from '../auth/JwtPayload.js';
24
+ import { ParameterSetLimitExceededError } from '../storage/storage-index.js';
25
25
  import { SyncContext } from './SyncContext.js';
26
26
  import { getIntersection, hasIntersection } from './util.js';
27
27
 
@@ -119,16 +119,38 @@ export class BucketChecksumState {
119
119
  const storage = this.bucketStorage;
120
120
 
121
121
  const update = await this.parameterState.getCheckpointUpdate(next);
122
- const { buckets: allBuckets, updatedBuckets, parameterQueryResultsByDefinition } = update;
122
+ const { buckets: allBuckets, updatedBuckets, usedParameterResults } = update;
123
123
 
124
124
  /** Set of all buckets in this checkpoint. */
125
125
  const bucketDescriptionMap = new Map(allBuckets.map((b) => [b.bucket, b]));
126
126
 
127
127
  if (bucketDescriptionMap.size > this.context.maxBuckets) {
128
- throw new ServiceError(
128
+ const error = new ServiceError(
129
129
  ErrorCode.PSYNC_S2305,
130
130
  `Too many buckets: ${bucketDescriptionMap.size} (limit of ${this.context.maxBuckets})`
131
131
  );
132
+
133
+ let errorMessage = error.message;
134
+ const logData: any = {
135
+ checkpoint: next.base.checkpoint,
136
+ user_id: this.parameterState.syncParams.userId,
137
+ buckets: allBuckets.length
138
+ };
139
+
140
+ // Count buckets per definition.
141
+ const bucketsByDefinition = new Map<string, number>();
142
+ for (const bucket of bucketDescriptionMap.values()) {
143
+ const definition = bucket.definition;
144
+ const count = bucketsByDefinition.get(definition) ?? 0;
145
+ bucketsByDefinition.set(definition, count + 1);
146
+ }
147
+
148
+ const breakdown = formatBucketDefinitionBreakdown(bucketsByDefinition);
149
+ errorMessage += breakdown.message;
150
+ logData.buckets_by_definition = breakdown.countsByDefinition;
151
+
152
+ this.logger.error(errorMessage, logData);
153
+ throw error;
132
154
  }
133
155
 
134
156
  let checksumMap: util.ChecksumMap;
@@ -212,13 +234,10 @@ export class BucketChecksumState {
212
234
  });
213
235
 
214
236
  deferredLog = () => {
215
- const totalParamResults = computeTotalParamResults(parameterQueryResultsByDefinition);
216
237
  let message = `Updated checkpoint: ${base.checkpoint} | `;
217
238
  message += `write: ${writeCheckpoint} | `;
218
239
  message += `buckets: ${allBuckets.length} | `;
219
- if (totalParamResults !== undefined) {
220
- message += `param_results: ${totalParamResults} | `;
221
- }
240
+ message += `param_results: ${usedParameterResults} | `;
222
241
  message += `updated: ${limitedBuckets(diff.updatedBuckets, 20)} | `;
223
242
  message += `removed: ${limitedBuckets(diff.removedBuckets, 20)}`;
224
243
  logCheckpoint(
@@ -231,7 +250,7 @@ export class BucketChecksumState {
231
250
  updated: diff.updatedBuckets.length,
232
251
  removed: diff.removedBuckets.length
233
252
  },
234
- totalParamResults
253
+ usedParameterResults
235
254
  );
236
255
  };
237
256
 
@@ -245,12 +264,9 @@ export class BucketChecksumState {
245
264
  } satisfies util.StreamingSyncCheckpointDiff;
246
265
  } else {
247
266
  deferredLog = () => {
248
- const totalParamResults = computeTotalParamResults(parameterQueryResultsByDefinition);
249
267
  let message = `New checkpoint: ${base.checkpoint} | write: ${writeCheckpoint} | `;
250
268
  message += `buckets: ${allBuckets.length}`;
251
- if (totalParamResults !== undefined) {
252
- message += ` | param_results: ${totalParamResults}`;
253
- }
269
+ message += ` | param_results: ${usedParameterResults}`;
254
270
  message += ` ${limitedBuckets(allBuckets, 20)}`;
255
271
  logCheckpoint(
256
272
  this.logger,
@@ -260,7 +276,7 @@ export class BucketChecksumState {
260
276
  user_id: userIdForLogs,
261
277
  buckets: allBuckets.length
262
278
  },
263
- totalParamResults
279
+ usedParameterResults
264
280
  );
265
281
  };
266
282
  bucketsToFetch = allBuckets;
@@ -393,11 +409,8 @@ export interface CheckpointUpdate {
393
409
  */
394
410
  updatedBuckets: Set<string> | typeof INVALIDATE_ALL_BUCKETS;
395
411
 
396
- /**
397
- * Number of parameter query results per sync stream definition (before deduplication).
398
- * Map from definition name to count.
399
- */
400
- parameterQueryResultsByDefinition?: Map<string, number>;
412
+ /** The amount of rows fetched from parameters indexes. */
413
+ usedParameterResults: number;
401
414
  }
402
415
 
403
416
  export class BucketParameterState {
@@ -445,6 +458,7 @@ export class BucketParameterState {
445
458
  const subscription = explicitStreamSubscriptions[i];
446
459
 
447
460
  const syncRuleStream: RequestedStream = {
461
+ priorityOverride: subscription.override_priority as BucketPriority | null,
448
462
  parameters: subscription.parameters ?? {},
449
463
  opaque_id: i
450
464
  };
@@ -480,25 +494,9 @@ export class BucketParameterState {
480
494
  * reference default buckets by their stream index instead of duplicating the name on wire.
481
495
  */
482
496
  translateResolvedBucket(description: ResolvedBucket, lookupIndex: Map<string, number>): util.ClientBucketDescription {
483
- // If the client is overriding the priority of any stream that yields this bucket, sync the bucket with that
484
- // priority.
485
- let priorityOverride: BucketPriority | null = null;
486
- for (const reason of description.inclusion_reasons) {
487
- if (reason != 'default') {
488
- const requestedPriority = this.explicitStreamSubscriptions[reason.subscription]?.override_priority;
489
- if (requestedPriority != null) {
490
- if (priorityOverride == null) {
491
- priorityOverride = requestedPriority as BucketPriority;
492
- } else {
493
- priorityOverride = Math.min(requestedPriority, priorityOverride) as BucketPriority;
494
- }
495
- }
496
- }
497
- }
498
-
499
497
  return {
500
498
  bucket: description.bucket,
501
- priority: priorityOverride ?? description.priority,
499
+ priority: description.priority,
502
500
  subscriptions: description.inclusion_reasons.map((reason) => {
503
501
  if (reason == 'default') {
504
502
  const stream = description.definition;
@@ -518,36 +516,41 @@ export class BucketParameterState {
518
516
  const querier = this.querier;
519
517
  let update: CheckpointUpdate;
520
518
  if (querier.hasDynamicBuckets) {
521
- update = await this.getCheckpointUpdateDynamic(checkpoint);
522
- } else {
523
- update = await this.getCheckpointUpdateStatic(checkpoint);
524
- }
525
-
526
- if (update.buckets.length > this.context.maxParameterQueryResults) {
527
- // TODO: Limit number of results even before we get to this point
528
- // This limit applies _before_ we get the unique set
529
- const error = new ServiceError(
530
- ErrorCode.PSYNC_S2305,
531
- `Too many parameter query results: ${update.buckets.length} (limit of ${this.context.maxParameterQueryResults})`
532
- );
519
+ try {
520
+ update = await this.getCheckpointUpdateDynamic(checkpoint);
521
+ } catch (e: unknown) {
522
+ if (e instanceof ParameterSetLimitExceededError) {
523
+ // Too many parameter results, create a breakdown of which streams are responsible for the most queries and
524
+ // then abort.
525
+ const error = new ServiceError(
526
+ ErrorCode.PSYNC_S2305,
527
+ `Too many parameter query results (limit of ${this.context.maxParameterQueryResults})`
528
+ );
529
+
530
+ let errorMessage = error.message;
531
+ const logData: any = {
532
+ checkpoint: checkpoint.base.checkpoint,
533
+ user_id: this.syncParams.userId
534
+ };
535
+
536
+ if (e.breakdown) {
537
+ const breakdown = formatParameterQueryBreakdown(e.breakdown);
538
+ if (breakdown) {
539
+ errorMessage += breakdown.message;
540
+ logData.parameterResults = breakdown.largestResults;
541
+ }
542
+ }
533
543
 
534
- let errorMessage = error.message;
535
- const logData: any = {
536
- checkpoint: checkpoint,
537
- user_id: this.syncParams.userId,
538
- parameter_query_results: update.buckets.length
539
- };
544
+ this.logger.error(errorMessage, logData);
545
+ throw error;
546
+ }
540
547
 
541
- if (update.parameterQueryResultsByDefinition && update.parameterQueryResultsByDefinition.size > 0) {
542
- const breakdown = formatParameterQueryBreakdown(update.parameterQueryResultsByDefinition);
543
- errorMessage += breakdown.message;
544
- logData.parameter_query_results_by_definition = breakdown.countsByDefinition;
548
+ throw e;
545
549
  }
546
-
547
- this.logger.error(errorMessage, logData);
548
-
549
- throw error;
550
+ } else {
551
+ update = await this.getCheckpointUpdateStatic(checkpoint);
550
552
  }
553
+
551
554
  return update;
552
555
  }
553
556
 
@@ -561,14 +564,16 @@ export class BucketParameterState {
561
564
  if (update.invalidateDataBuckets) {
562
565
  return {
563
566
  buckets: staticBuckets,
564
- updatedBuckets: INVALIDATE_ALL_BUCKETS
567
+ updatedBuckets: INVALIDATE_ALL_BUCKETS,
568
+ usedParameterResults: 0
565
569
  };
566
570
  }
567
571
 
568
572
  const updatedBuckets = new Set<string>(getIntersection(this.staticBuckets, update.updatedDataBuckets));
569
573
  return {
570
574
  buckets: staticBuckets,
571
- updatedBuckets
575
+ updatedBuckets,
576
+ usedParameterResults: 0
572
577
  };
573
578
  }
574
579
 
@@ -600,27 +605,55 @@ export class BucketParameterState {
600
605
  }
601
606
 
602
607
  let dynamicBuckets: ResolvedBucket[];
603
- let parameterQueryResultsByDefinition: Map<string, number> | undefined;
608
+ let usedParameterResults = 0;
604
609
  if (hasParameterChange || this.cachedDynamicBuckets == null || this.cachedDynamicBucketSet == null) {
605
610
  const recordedLookups = new Set<string>();
611
+ const parameterLimit = this.context.maxParameterQueryResults;
612
+ let remainingBudget = parameterLimit;
613
+
614
+ // Log of lookups to provide a breakdown if we exceed the dynamic lookup limit.
615
+ // FIXME: This is horrible, Sync Streams will invoke the callback concurrently and we can't properly deal with
616
+ // that. We should replace queriers for Sync Streams with an explicit graph structure based on sync plans instead
617
+ // of adding these checks to the imperative querier interface.
618
+ const lookupLog: storage.ParameterQueryInvocationLog[] = [];
606
619
 
607
620
  dynamicBuckets = await querier.queryDynamicBucketDescriptions({
608
- getParameterSets(lookups) {
621
+ getParameterSets: async (lookups, definition) => {
622
+ if (lookups.length > parameterLimit) {
623
+ // Sync Streams can chain parameter lookups, so a large output from an earlier call may become the input
624
+ // here. We reuse the output limit as a generous upper bound; legitimate queries are much smaller.
625
+ const msg = `Attempted to fetch ${lookups.length} lookups at once, a maximum of ${parameterLimit} lookups are allowed.`;
626
+ this.logger.error(msg, {
627
+ user_id: this.syncParams.userId,
628
+ checkpoint: checkpoint.base.checkpoint,
629
+ cause: definition
630
+ });
631
+ throw new ServiceError(ErrorCode.PSYNC_S2305, msg);
632
+ }
633
+
609
634
  for (const lookup of lookups) {
610
635
  recordedLookups.add(lookup.serializedRepresentation);
611
636
  }
612
637
 
613
- return checkpoint.base.getParameterSets(lookups);
638
+ try {
639
+ const results = await checkpoint.base.getParameterSets(lookups, remainingBudget);
640
+ const numRows = results.reduce((a, b) => a + b.rows.length, 0);
641
+
642
+ lookupLog.push({ definition, resultsOrLimit: numRows, didExceedLimit: false });
643
+ remainingBudget -= numRows;
644
+ usedParameterResults += numRows;
645
+ return results;
646
+ } catch (e: unknown) {
647
+ if (e instanceof ParameterSetLimitExceededError) {
648
+ lookupLog.push({ definition, resultsOrLimit: remainingBudget, didExceedLimit: true });
649
+ throw new ParameterSetLimitExceededError(parameterLimit, lookupLog);
650
+ }
651
+
652
+ throw e;
653
+ }
614
654
  }
615
655
  });
616
656
 
617
- // Count parameter query results per definition (before deduplication)
618
- parameterQueryResultsByDefinition = new Map<string, number>();
619
- for (const bucket of dynamicBuckets) {
620
- const count = parameterQueryResultsByDefinition.get(bucket.definition) ?? 0;
621
- parameterQueryResultsByDefinition.set(bucket.definition, count + 1);
622
- }
623
-
624
657
  this.cachedDynamicBuckets = dynamicBuckets;
625
658
  this.cachedDynamicBucketSet = new Set<string>(dynamicBuckets.map((b) => b.bucket));
626
659
  this.lookupsFromPreviousCheckpoint = recordedLookups;
@@ -644,13 +677,13 @@ export class BucketParameterState {
644
677
  buckets: allBuckets,
645
678
  // We cannot track individual bucket updates for dynamic lookups yet
646
679
  updatedBuckets: INVALIDATE_ALL_BUCKETS,
647
- parameterQueryResultsByDefinition
680
+ usedParameterResults
648
681
  };
649
682
  } else {
650
683
  return {
651
684
  buckets: allBuckets,
652
685
  updatedBuckets: updatedBuckets,
653
- parameterQueryResultsByDefinition
686
+ usedParameterResults
654
687
  };
655
688
  }
656
689
  }
@@ -684,18 +717,6 @@ export interface CheckpointLine {
684
717
  // Use a more specific type to simplify testing
685
718
  export type BucketChecksumStateStorage = Pick<storage.SyncRulesBucketStorage, 'getChecksums'>;
686
719
 
687
- /**
688
- * Compute the total number of parameter query results across all definitions.
689
- */
690
- function computeTotalParamResults(
691
- parameterQueryResultsByDefinition: Map<string, number> | undefined
692
- ): number | undefined {
693
- if (!parameterQueryResultsByDefinition) {
694
- return undefined;
695
- }
696
- return Array.from(parameterQueryResultsByDefinition.values()).reduce((sum, count) => sum + count, 0);
697
- }
698
-
699
720
  /**
700
721
  * Log a checkpoint message, enriching it with parameter query result counts if available.
701
722
  *
@@ -717,20 +738,20 @@ function logCheckpoint(
717
738
  }
718
739
 
719
740
  /**
720
- * Format a breakdown of parameter query results by sync rule definition.
741
+ * Format a breakdown of dynamic bucket by sync stream definition.
721
742
  *
722
743
  * Sorts definitions by count (descending), includes the top 10, and returns both the
723
744
  * formatted message string and the counts record suitable for structured log data.
724
745
  */
725
- function formatParameterQueryBreakdown(parameterQueryResultsByDefinition: Map<string, number>): {
746
+ function formatBucketDefinitionBreakdown(bucketsByDefinition: Map<string, number>): {
726
747
  message: string;
727
748
  countsByDefinition: Record<string, number>;
728
749
  } {
729
750
  // Sort definitions by count (descending) and take top 10
730
- const allSorted = Array.from(parameterQueryResultsByDefinition.entries()).sort((a, b) => b[1] - a[1]);
751
+ const allSorted = Array.from(bucketsByDefinition.entries()).sort((a, b) => b[1] - a[1]);
731
752
  const sortedDefinitions = allSorted.slice(0, 10);
732
753
 
733
- let message = '\nParameter query results by definition:';
754
+ let message = '\Buckets by definition:';
734
755
  const countsByDefinition: Record<string, number> = {};
735
756
  for (const [definition, count] of sortedDefinitions) {
736
757
  message += `\n ${definition}: ${count}`;
@@ -746,6 +767,33 @@ function formatParameterQueryBreakdown(parameterQueryResultsByDefinition: Map<st
746
767
  return { message, countsByDefinition };
747
768
  }
748
769
 
770
+ function formatParameterQueryBreakdown(log: storage.ParameterQueryInvocationLog[]) {
771
+ if (log.length == 0) {
772
+ return;
773
+ }
774
+
775
+ // When an exception about too many parameter results is thrown, the last entry is the one that finally exceeded the
776
+ // limit.
777
+ const results = Array.from(log);
778
+ const failure = results.pop()!;
779
+
780
+ let message = '\nInvoked parameter queries by definition:';
781
+ results.sort((a, b) => b.resultsOrLimit - a.resultsOrLimit);
782
+ const largestResults = results.splice(0, 9);
783
+
784
+ for (const entry of largestResults) {
785
+ message += `\n ${entry.definition}: ${entry.resultsOrLimit} results.`;
786
+ }
787
+
788
+ if (largestResults.length < log.length) {
789
+ message += `\n ... and ${log.length - largestResults.length} more invocations`;
790
+ }
791
+
792
+ message += `\n ${failure.definition} exceeded the remaining limit of ${failure.resultsOrLimit} available results.`;
793
+ largestResults.push(failure);
794
+ return { message, largestResults };
795
+ }
796
+
749
797
  function limitedBuckets(buckets: string[] | { bucket: string }[], limit: number) {
750
798
  buckets = buckets.map((b) => {
751
799
  if (typeof b != 'string') {
@@ -1,8 +1,8 @@
1
1
  import { MetricsEngine } from '../metrics/MetricsEngine.js';
2
2
 
3
+ import { ServiceAssertionError } from '@powersync/lib-services-framework';
3
4
  import { APIMetric } from '@powersync/service-types';
4
5
  import { SyncBucketData } from '../util/protocol-types.js';
5
- import { ServiceAssertionError } from '@powersync/lib-services-framework';
6
6
 
7
7
  /**
8
8
  * Record sync stats per request stream.
@@ -1,6 +1,6 @@
1
+ export * from './BucketChecksumState.js';
1
2
  export * from './RequestTracker.js';
2
3
  export * from './safeRace.js';
3
4
  export * from './sync.js';
4
- export * from './util.js';
5
- export * from './BucketChecksumState.js';
6
5
  export * from './SyncContext.js';
6
+ export * from './util.js';
package/src/sync/sync.ts CHANGED
@@ -1,11 +1,5 @@
1
1
  import { JSONBig, JsonContainer } from '@powersync/service-jsonbig';
2
- import {
3
- BucketDescription,
4
- BucketPriority,
5
- HydratedSyncRules,
6
- ResolvedBucket,
7
- SqliteJsonValue
8
- } from '@powersync/service-sync-rules';
2
+ import { BucketPriority, HydratedSyncRules, ResolvedBucket, SqliteJsonValue } from '@powersync/service-sync-rules';
9
3
 
10
4
  import { AbortError } from 'ix/aborterror.js';
11
5
 
@@ -273,7 +267,7 @@ interface BucketDataRequest {
273
267
  * This signal also fires when abort_connection fires.
274
268
  */
275
269
  abort_batch: AbortSignal;
276
- /** User id for debug purposes, not for sync rules. */
270
+ /** User id for debug purposes, not for sync config. */
277
271
  userIdForLogs?: SqliteJsonValue;
278
272
  forPriority: BucketPriority | null;
279
273
  onRowsSent: (stats: OperationsSentStats) => void;
package/src/sync/util.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as timers from 'timers/promises';
2
2
 
3
3
  import { SemaphoreInterface } from 'async-mutex';
4
+ import { serialize } from 'bson';
4
5
  import * as util from '../util/util-index.js';
5
6
  import { RequestTracker } from './RequestTracker.js';
6
- import { serialize } from 'bson';
7
7
 
8
8
  export type TokenStreamOptions = {
9
9
  /**