@powersync/service-core 0.0.0-dev-20250310210938 → 0.0.0-dev-20250312090341

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 (100) hide show
  1. package/CHANGELOG.md +2 -7
  2. package/dist/api/api-index.d.ts +0 -1
  3. package/dist/api/api-index.js +0 -1
  4. package/dist/api/api-index.js.map +1 -1
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.js +2 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/metrics/Metrics.d.ts +30 -0
  9. package/dist/metrics/Metrics.js +202 -0
  10. package/dist/metrics/Metrics.js.map +1 -0
  11. package/dist/replication/AbstractReplicationJob.d.ts +0 -2
  12. package/dist/replication/AbstractReplicationJob.js.map +1 -1
  13. package/dist/replication/AbstractReplicator.d.ts +0 -3
  14. package/dist/replication/AbstractReplicator.js +0 -3
  15. package/dist/replication/AbstractReplicator.js.map +1 -1
  16. package/dist/replication/ReplicationModule.d.ts +0 -7
  17. package/dist/replication/ReplicationModule.js +0 -1
  18. package/dist/replication/ReplicationModule.js.map +1 -1
  19. package/dist/replication/replication-index.d.ts +0 -1
  20. package/dist/replication/replication-index.js +0 -1
  21. package/dist/replication/replication-index.js.map +1 -1
  22. package/dist/routes/endpoints/socket-route.js +5 -5
  23. package/dist/routes/endpoints/socket-route.js.map +1 -1
  24. package/dist/routes/endpoints/sync-stream.js +6 -6
  25. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  26. package/dist/storage/SyncRulesBucketStorage.d.ts +2 -1
  27. package/dist/storage/SyncRulesBucketStorage.js +1 -1
  28. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  29. package/dist/storage/bson.d.ts +1 -0
  30. package/dist/storage/bson.js +6 -10
  31. package/dist/storage/bson.js.map +1 -1
  32. package/dist/storage/storage-index.d.ts +0 -1
  33. package/dist/storage/storage-index.js +0 -1
  34. package/dist/storage/storage-index.js.map +1 -1
  35. package/dist/sync/BucketChecksumState.d.ts +3 -0
  36. package/dist/sync/BucketChecksumState.js +51 -21
  37. package/dist/sync/BucketChecksumState.js.map +1 -1
  38. package/dist/sync/RequestTracker.d.ts +0 -3
  39. package/dist/sync/RequestTracker.js +3 -8
  40. package/dist/sync/RequestTracker.js.map +1 -1
  41. package/dist/sync/util.d.ts +1 -0
  42. package/dist/sync/util.js +11 -0
  43. package/dist/sync/util.js.map +1 -1
  44. package/dist/system/ServiceContext.d.ts +3 -3
  45. package/dist/system/ServiceContext.js +3 -7
  46. package/dist/system/ServiceContext.js.map +1 -1
  47. package/dist/util/config/compound-config-collector.js +1 -2
  48. package/dist/util/config/compound-config-collector.js.map +1 -1
  49. package/package.json +7 -7
  50. package/src/api/api-index.ts +0 -1
  51. package/src/index.ts +2 -2
  52. package/src/metrics/Metrics.ts +255 -0
  53. package/src/replication/AbstractReplicationJob.ts +0 -2
  54. package/src/replication/AbstractReplicator.ts +0 -7
  55. package/src/replication/ReplicationModule.ts +0 -10
  56. package/src/replication/replication-index.ts +0 -1
  57. package/src/routes/endpoints/socket-route.ts +5 -6
  58. package/src/routes/endpoints/sync-stream.ts +6 -7
  59. package/src/storage/SyncRulesBucketStorage.ts +3 -2
  60. package/src/storage/bson.ts +7 -9
  61. package/src/storage/storage-index.ts +0 -1
  62. package/src/sync/BucketChecksumState.ts +54 -22
  63. package/src/sync/RequestTracker.ts +3 -9
  64. package/src/sync/util.ts +12 -0
  65. package/src/system/ServiceContext.ts +4 -9
  66. package/src/util/config/compound-config-collector.ts +1 -2
  67. package/test/src/sync/BucketChecksumState.test.ts +3 -2
  68. package/tsconfig.tsbuildinfo +1 -1
  69. package/dist/api/api-metrics.d.ts +0 -11
  70. package/dist/api/api-metrics.js +0 -30
  71. package/dist/api/api-metrics.js.map +0 -1
  72. package/dist/metrics/MetricsEngine.d.ts +0 -21
  73. package/dist/metrics/MetricsEngine.js +0 -79
  74. package/dist/metrics/MetricsEngine.js.map +0 -1
  75. package/dist/metrics/metrics-index.d.ts +0 -4
  76. package/dist/metrics/metrics-index.js +0 -5
  77. package/dist/metrics/metrics-index.js.map +0 -1
  78. package/dist/metrics/metrics-interfaces.d.ts +0 -36
  79. package/dist/metrics/metrics-interfaces.js +0 -6
  80. package/dist/metrics/metrics-interfaces.js.map +0 -1
  81. package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.d.ts +0 -10
  82. package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js +0 -51
  83. package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js.map +0 -1
  84. package/dist/metrics/open-telemetry/util.d.ts +0 -6
  85. package/dist/metrics/open-telemetry/util.js +0 -56
  86. package/dist/metrics/open-telemetry/util.js.map +0 -1
  87. package/dist/replication/replication-metrics.d.ts +0 -11
  88. package/dist/replication/replication-metrics.js +0 -39
  89. package/dist/replication/replication-metrics.js.map +0 -1
  90. package/dist/storage/storage-metrics.d.ts +0 -4
  91. package/dist/storage/storage-metrics.js +0 -56
  92. package/dist/storage/storage-metrics.js.map +0 -1
  93. package/src/api/api-metrics.ts +0 -35
  94. package/src/metrics/MetricsEngine.ts +0 -98
  95. package/src/metrics/metrics-index.ts +0 -4
  96. package/src/metrics/metrics-interfaces.ts +0 -41
  97. package/src/metrics/open-telemetry/OpenTelemetryMetricsFactory.ts +0 -66
  98. package/src/metrics/open-telemetry/util.ts +0 -74
  99. package/src/replication/replication-metrics.ts +0 -45
  100. package/src/storage/storage-metrics.ts +0 -67
@@ -0,0 +1,255 @@
1
+ import { Attributes, Counter, ObservableGauge, UpDownCounter, ValueType } from '@opentelemetry/api';
2
+ import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
3
+ import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
4
+ import { Resource } from '@opentelemetry/resources';
5
+ import { MeterProvider, MetricReader, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
6
+ import { logger, ServiceAssertionError } from '@powersync/lib-services-framework';
7
+ import * as storage from '../storage/storage-index.js';
8
+ import * as util from '../util/util-index.js';
9
+
10
+ export interface MetricsOptions {
11
+ disable_telemetry_sharing: boolean;
12
+ powersync_instance_id: string;
13
+ internal_metrics_endpoint: string;
14
+ }
15
+
16
+ export class Metrics {
17
+ private static instance: Metrics;
18
+
19
+ private prometheusExporter: PrometheusExporter;
20
+ private meterProvider: MeterProvider;
21
+
22
+ // Metrics
23
+ // 1. Data processing / month
24
+
25
+ // 1a. Postgres -> PowerSync
26
+ // Record on replication pod
27
+ public data_replicated_bytes: Counter<Attributes>;
28
+ // 1b. PowerSync -> clients
29
+ // Record on API pod
30
+ public data_synced_bytes: Counter<Attributes>;
31
+ // Unused for pricing
32
+ // Record on replication pod
33
+ public rows_replicated_total: Counter<Attributes>;
34
+ // Unused for pricing
35
+ // Record on replication pod
36
+ public transactions_replicated_total: Counter<Attributes>;
37
+ // Unused for pricing
38
+ // Record on replication pod
39
+ public chunks_replicated_total: Counter<Attributes>;
40
+
41
+ // 2. Sync operations / month
42
+
43
+ // Record on API pod
44
+ public operations_synced_total: Counter<Attributes>;
45
+
46
+ // 3. Data hosted on PowerSync sync service
47
+
48
+ // Record on replication pod
49
+ // 3a. Replication storage -> raw data as received from Postgres.
50
+ public replication_storage_size_bytes: ObservableGauge<Attributes>;
51
+ // 3b. Operations storage -> transformed history, as will be synced to clients
52
+ public operation_storage_size_bytes: ObservableGauge<Attributes>;
53
+ // 3c. Parameter storage -> used for parameter queries
54
+ public parameter_storage_size_bytes: ObservableGauge<Attributes>;
55
+
56
+ // 4. Peak concurrent connections
57
+
58
+ // Record on API pod
59
+ public concurrent_connections: UpDownCounter<Attributes>;
60
+
61
+ private constructor(meterProvider: MeterProvider, prometheusExporter: PrometheusExporter) {
62
+ this.meterProvider = meterProvider;
63
+ this.prometheusExporter = prometheusExporter;
64
+ const meter = meterProvider.getMeter('powersync');
65
+
66
+ this.data_replicated_bytes = meter.createCounter('powersync_data_replicated_bytes_total', {
67
+ description: 'Uncompressed size of replicated data',
68
+ unit: 'bytes',
69
+ valueType: ValueType.INT
70
+ });
71
+
72
+ this.data_synced_bytes = meter.createCounter('powersync_data_synced_bytes_total', {
73
+ description: 'Uncompressed size of synced data',
74
+ unit: 'bytes',
75
+ valueType: ValueType.INT
76
+ });
77
+
78
+ this.rows_replicated_total = meter.createCounter('powersync_rows_replicated_total', {
79
+ description: 'Total number of replicated rows',
80
+ valueType: ValueType.INT
81
+ });
82
+
83
+ this.transactions_replicated_total = meter.createCounter('powersync_transactions_replicated_total', {
84
+ description: 'Total number of replicated transactions',
85
+ valueType: ValueType.INT
86
+ });
87
+
88
+ this.chunks_replicated_total = meter.createCounter('powersync_chunks_replicated_total', {
89
+ description: 'Total number of replication chunks',
90
+ valueType: ValueType.INT
91
+ });
92
+
93
+ this.operations_synced_total = meter.createCounter('powersync_operations_synced_total', {
94
+ description: 'Number of operations synced',
95
+ valueType: ValueType.INT
96
+ });
97
+
98
+ this.replication_storage_size_bytes = meter.createObservableGauge('powersync_replication_storage_size_bytes', {
99
+ description: 'Size of current data stored in PowerSync',
100
+ unit: 'bytes',
101
+ valueType: ValueType.INT
102
+ });
103
+
104
+ this.operation_storage_size_bytes = meter.createObservableGauge('powersync_operation_storage_size_bytes', {
105
+ description: 'Size of operations stored in PowerSync',
106
+ unit: 'bytes',
107
+ valueType: ValueType.INT
108
+ });
109
+
110
+ this.parameter_storage_size_bytes = meter.createObservableGauge('powersync_parameter_storage_size_bytes', {
111
+ description: 'Size of parameter data stored in PowerSync',
112
+ unit: 'bytes',
113
+ valueType: ValueType.INT
114
+ });
115
+
116
+ this.concurrent_connections = meter.createUpDownCounter('powersync_concurrent_connections', {
117
+ description: 'Number of concurrent sync connections',
118
+ valueType: ValueType.INT
119
+ });
120
+ }
121
+
122
+ // Generally only useful for tests. Note: gauges are ignored here.
123
+ resetCounters() {
124
+ this.data_replicated_bytes.add(0);
125
+ this.data_synced_bytes.add(0);
126
+ this.rows_replicated_total.add(0);
127
+ this.transactions_replicated_total.add(0);
128
+ this.chunks_replicated_total.add(0);
129
+ this.operations_synced_total.add(0);
130
+ this.concurrent_connections.add(0);
131
+ }
132
+
133
+ public static getInstance(): Metrics {
134
+ if (!Metrics.instance) {
135
+ throw new ServiceAssertionError('Metrics have not been initialized');
136
+ }
137
+
138
+ return Metrics.instance;
139
+ }
140
+
141
+ public static async initialise(options: MetricsOptions): Promise<void> {
142
+ if (Metrics.instance) {
143
+ return;
144
+ }
145
+ logger.info('Configuring telemetry.');
146
+
147
+ logger.info(
148
+ `
149
+ Attention:
150
+ PowerSync collects completely anonymous telemetry regarding usage.
151
+ This information is used to shape our roadmap to better serve our customers.
152
+ You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
153
+ https://docs.powersync.com/self-hosting/telemetry
154
+ Anonymous telemetry is currently: ${options.disable_telemetry_sharing ? 'disabled' : 'enabled'}
155
+ `.trim()
156
+ );
157
+
158
+ const configuredExporters: MetricReader[] = [];
159
+
160
+ const port: number = util.env.METRICS_PORT ?? 0;
161
+ const prometheusExporter = new PrometheusExporter({ port: port, preventServerStart: true });
162
+ configuredExporters.push(prometheusExporter);
163
+
164
+ if (!options.disable_telemetry_sharing) {
165
+ logger.info('Sharing anonymous telemetry');
166
+ const periodicExporter = new PeriodicExportingMetricReader({
167
+ exporter: new OTLPMetricExporter({
168
+ url: options.internal_metrics_endpoint
169
+ }),
170
+ exportIntervalMillis: 1000 * 60 * 5 // 5 minutes
171
+ });
172
+
173
+ configuredExporters.push(periodicExporter);
174
+ }
175
+
176
+ const meterProvider = new MeterProvider({
177
+ resource: new Resource({
178
+ ['service']: 'PowerSync',
179
+ ['instance_id']: options.powersync_instance_id
180
+ }),
181
+ readers: configuredExporters
182
+ });
183
+
184
+ if (port > 0) {
185
+ await prometheusExporter.startServer();
186
+ }
187
+
188
+ Metrics.instance = new Metrics(meterProvider, prometheusExporter);
189
+
190
+ logger.info('Telemetry configuration complete.');
191
+ }
192
+
193
+ public async shutdown(): Promise<void> {
194
+ await this.meterProvider.shutdown();
195
+ }
196
+
197
+ public configureApiMetrics() {
198
+ // Initialize the metric, so that it reports a value before connections
199
+ // have been opened.
200
+ this.concurrent_connections.add(0);
201
+ }
202
+
203
+ public configureReplicationMetrics(bucketStorage: storage.BucketStorageFactory) {
204
+ // Rate limit collection of these stats, since it may be an expensive query
205
+ const MINIMUM_INTERVAL = 60_000;
206
+
207
+ let cachedRequest: Promise<storage.StorageMetrics | null> | undefined = undefined;
208
+ let cacheTimestamp = 0;
209
+
210
+ function getMetrics() {
211
+ if (cachedRequest == null || Date.now() - cacheTimestamp > MINIMUM_INTERVAL) {
212
+ cachedRequest = bucketStorage.getStorageMetrics().catch((e) => {
213
+ logger.error(`Failed to get storage metrics`, e);
214
+ return null;
215
+ });
216
+ cacheTimestamp = Date.now();
217
+ }
218
+ return cachedRequest;
219
+ }
220
+
221
+ this.operation_storage_size_bytes.addCallback(async (result) => {
222
+ const metrics = await getMetrics();
223
+ if (metrics) {
224
+ result.observe(metrics.operations_size_bytes);
225
+ }
226
+ });
227
+
228
+ this.parameter_storage_size_bytes.addCallback(async (result) => {
229
+ const metrics = await getMetrics();
230
+ if (metrics) {
231
+ result.observe(metrics.parameters_size_bytes);
232
+ }
233
+ });
234
+
235
+ this.replication_storage_size_bytes.addCallback(async (result) => {
236
+ const metrics = await getMetrics();
237
+ if (metrics) {
238
+ result.observe(metrics.replication_size_bytes);
239
+ }
240
+ });
241
+ }
242
+
243
+ public async getMetricValueForTests(name: string): Promise<number | undefined> {
244
+ const metrics = await this.prometheusExporter.collect();
245
+ const scoped = metrics.resourceMetrics.scopeMetrics[0].metrics;
246
+ const metric = scoped.find((metric) => metric.descriptor.name == name);
247
+ if (metric == null) {
248
+ throw new Error(
249
+ `Cannot find metric ${name}. Options: ${scoped.map((metric) => metric.descriptor.name).join(',')}`
250
+ );
251
+ }
252
+ const point = metric.dataPoints[metric.dataPoints.length - 1];
253
+ return point?.value as number;
254
+ }
255
+ }
@@ -2,12 +2,10 @@ import { container, logger } from '@powersync/lib-services-framework';
2
2
  import winston from 'winston';
3
3
  import * as storage from '../storage/storage-index.js';
4
4
  import { ErrorRateLimiter } from './ErrorRateLimiter.js';
5
- import { MetricsEngine } from '../metrics/MetricsEngine.js';
6
5
 
7
6
  export interface AbstractReplicationJobOptions {
8
7
  id: string;
9
8
  storage: storage.SyncRulesBucketStorage;
10
- metrics: MetricsEngine;
11
9
  lock: storage.ReplicationLock;
12
10
  rateLimiter: ErrorRateLimiter;
13
11
  }
@@ -7,7 +7,6 @@ import { SyncRulesProvider } from '../util/config/sync-rules/sync-rules-provider
7
7
  import { AbstractReplicationJob } from './AbstractReplicationJob.js';
8
8
  import { ErrorRateLimiter } from './ErrorRateLimiter.js';
9
9
  import { ConnectionTestResult } from './ReplicationModule.js';
10
- import { MetricsEngine } from '../metrics/MetricsEngine.js';
11
10
 
12
11
  // 5 minutes
13
12
  const PING_INTERVAL = 1_000_000_000n * 300n;
@@ -20,7 +19,6 @@ export interface CreateJobOptions {
20
19
  export interface AbstractReplicatorOptions {
21
20
  id: string;
22
21
  storageEngine: StorageEngine;
23
- metricsEngine: MetricsEngine;
24
22
  syncRuleProvider: SyncRulesProvider;
25
23
  /**
26
24
  * This limits the effect of retries when there is a persistent issue.
@@ -35,7 +33,6 @@ export interface AbstractReplicatorOptions {
35
33
  */
36
34
  export abstract class AbstractReplicator<T extends AbstractReplicationJob = AbstractReplicationJob> {
37
35
  protected logger: winston.Logger;
38
-
39
36
  /**
40
37
  * Map of replication jobs by sync rule id. Usually there is only one running job, but there could be two when
41
38
  * transitioning to a new set of sync rules.
@@ -75,10 +72,6 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
75
72
  return this.options.rateLimiter;
76
73
  }
77
74
 
78
- protected get metrics() {
79
- return this.options.metricsEngine;
80
- }
81
-
82
75
  public async start(): Promise<void> {
83
76
  this.runLoop().catch((e) => {
84
77
  this.logger.error('Data source fatal replication error', e);
@@ -64,14 +64,6 @@ export abstract class ReplicationModule<TConfig extends DataSourceConfig>
64
64
  */
65
65
  protected abstract createReplicator(context: system.ServiceContext): AbstractReplicator;
66
66
 
67
- /**
68
- * Any additional initialization specific to the module should be added here. Will be called if necessary after the
69
- * main initialization has been completed
70
- * @param context
71
- * @protected
72
- */
73
- protected abstract onInitialized(context: system.ServiceContext): Promise<void>;
74
-
75
67
  public abstract testConnection(config: TConfig): Promise<ConnectionTestResult>;
76
68
 
77
69
  /**
@@ -101,8 +93,6 @@ export abstract class ReplicationModule<TConfig extends DataSourceConfig>
101
93
 
102
94
  context.replicationEngine?.register(this.createReplicator(context));
103
95
  context.routerEngine?.registerAPI(this.createRouteAPIAdapter());
104
-
105
- await this.onInitialized(context);
106
96
  }
107
97
 
108
98
  protected decodeConfig(config: TConfig): void {
@@ -3,4 +3,3 @@ export * from './AbstractReplicator.js';
3
3
  export * from './ErrorRateLimiter.js';
4
4
  export * from './ReplicationEngine.js';
5
5
  export * from './ReplicationModule.js';
6
- export * from './replication-metrics.js';
@@ -2,19 +2,18 @@ import { ErrorCode, errors, logger, schema } from '@powersync/lib-services-frame
2
2
  import { RequestParameters } from '@powersync/service-sync-rules';
3
3
  import { serialize } from 'bson';
4
4
 
5
+ import { Metrics } from '../../metrics/Metrics.js';
5
6
  import * as sync from '../../sync/sync-index.js';
6
7
  import * as util from '../../util/util-index.js';
7
8
  import { SocketRouteGenerator } from '../router-socket.js';
8
9
  import { SyncRoutes } from './sync-stream.js';
9
10
 
10
- import { APIMetric } from '@powersync/service-types';
11
-
12
11
  export const syncStreamReactive: SocketRouteGenerator = (router) =>
13
12
  router.reactiveStream<util.StreamingSyncRequest, any>(SyncRoutes.STREAM, {
14
13
  validator: schema.createTsCodecValidator(util.StreamingSyncRequest, { allowAdditional: true }),
15
14
  handler: async ({ context, params, responder, observer, initialN, signal: upstreamSignal }) => {
16
15
  const { service_context } = context;
17
- const { routerEngine, metricsEngine, syncContext } = service_context;
16
+ const { routerEngine, syncContext } = service_context;
18
17
 
19
18
  // Create our own controller that we can abort directly
20
19
  const controller = new AbortController();
@@ -70,8 +69,8 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
70
69
  controller.abort();
71
70
  });
72
71
 
73
- metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(1);
74
- const tracker = new sync.RequestTracker(metricsEngine);
72
+ Metrics.getInstance().concurrent_connections.add(1);
73
+ const tracker = new sync.RequestTracker();
75
74
  try {
76
75
  for await (const data of sync.streamResponse({
77
76
  syncContext: syncContext,
@@ -148,7 +147,7 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
148
147
  operations_synced: tracker.operationsSynced,
149
148
  data_synced_bytes: tracker.dataSyncedBytes
150
149
  });
151
- metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(-1);
150
+ Metrics.getInstance().concurrent_connections.add(-1);
152
151
  }
153
152
  }
154
153
  });
@@ -5,11 +5,10 @@ import { Readable } from 'stream';
5
5
  import * as sync from '../../sync/sync-index.js';
6
6
  import * as util from '../../util/util-index.js';
7
7
 
8
+ import { Metrics } from '../../metrics/Metrics.js';
8
9
  import { authUser } from '../auth.js';
9
10
  import { routeDefinition } from '../router.js';
10
11
 
11
- import { APIMetric } from '@powersync/service-types';
12
-
13
12
  export enum SyncRoutes {
14
13
  STREAM = '/sync/stream'
15
14
  }
@@ -21,7 +20,7 @@ export const syncStreamed = routeDefinition({
21
20
  validator: schema.createTsCodecValidator(util.StreamingSyncRequest, { allowAdditional: true }),
22
21
  handler: async (payload) => {
23
22
  const { service_context } = payload.context;
24
- const { routerEngine, storageEngine, metricsEngine, syncContext } = service_context;
23
+ const { routerEngine, storageEngine, syncContext } = service_context;
25
24
  const headers = payload.request.headers;
26
25
  const userAgent = headers['x-user-agent'] ?? headers['user-agent'];
27
26
  const clientId = payload.params.client_id;
@@ -50,9 +49,9 @@ export const syncStreamed = routeDefinition({
50
49
  const syncRules = bucketStorage.getParsedSyncRules(routerEngine!.getAPI().getParseSyncRulesOptions());
51
50
 
52
51
  const controller = new AbortController();
53
- const tracker = new sync.RequestTracker(metricsEngine);
52
+ const tracker = new sync.RequestTracker();
54
53
  try {
55
- metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(1);
54
+ Metrics.getInstance().concurrent_connections.add(1);
56
55
  const stream = Readable.from(
57
56
  sync.transformToBytesTracked(
58
57
  sync.ndjson(
@@ -97,7 +96,7 @@ export const syncStreamed = routeDefinition({
97
96
  data: stream,
98
97
  afterSend: async () => {
99
98
  controller.abort();
100
- metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(-1);
99
+ Metrics.getInstance().concurrent_connections.add(-1);
101
100
  logger.info(`Sync stream complete`, {
102
101
  user_id: syncParams.user_id,
103
102
  client_id: clientId,
@@ -109,7 +108,7 @@ export const syncStreamed = routeDefinition({
109
108
  });
110
109
  } catch (ex) {
111
110
  controller.abort();
112
- metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(-1);
111
+ Metrics.getInstance().concurrent_connections.add(-1);
113
112
  }
114
113
  }
115
114
  });
@@ -253,13 +253,14 @@ export interface GetCheckpointChangesOptions {
253
253
  export interface CheckpointChanges {
254
254
  updatedDataBuckets: string[];
255
255
  invalidateDataBuckets: boolean;
256
- updatedParameterBucketDefinitions: string[];
256
+ /** Serialized using JSONBig */
257
+ updatedParameterLookups: Set<string>;
257
258
  invalidateParameterBuckets: boolean;
258
259
  }
259
260
 
260
261
  export const CHECKPOINT_INVALIDATE_ALL: CheckpointChanges = {
261
262
  updatedDataBuckets: [],
262
263
  invalidateDataBuckets: true,
263
- updatedParameterBucketDefinitions: [],
264
+ updatedParameterLookups: new Set<string>(),
264
265
  invalidateParameterBuckets: true
265
266
  };
@@ -25,22 +25,20 @@ export const BSON_DESERIALIZE_DATA_OPTIONS: bson.DeserializeOptions = {
25
25
  * @param lookup
26
26
  */
27
27
  export const serializeLookupBuffer = (lookup: SqliteJsonValue[]): NodeBuffer => {
28
- const normalized = lookup.map((value) => {
29
- if (typeof value == 'number' && Number.isInteger(value)) {
30
- return BigInt(value);
31
- } else {
32
- return value;
33
- }
34
- });
35
- return bson.serialize({ l: normalized }) as NodeBuffer;
28
+ return bson.serialize({ l: lookup }) as NodeBuffer;
36
29
  };
37
30
 
38
31
  export const serializeLookup = (lookup: SqliteJsonValue[]) => {
39
32
  return new bson.Binary(serializeLookupBuffer(lookup));
40
33
  };
41
34
 
42
- export const getLookupBucketDefinitionName = (lookup: bson.Binary) => {
35
+ export const deserializeParameterLookup = (lookup: bson.Binary) => {
43
36
  const parsed = bson.deserialize(lookup.buffer, BSON_DESERIALIZE_INTERNAL_OPTIONS).l as SqliteJsonValue[];
37
+ return parsed;
38
+ };
39
+
40
+ export const getLookupBucketDefinitionName = (lookup: bson.Binary) => {
41
+ const parsed = deserializeParameterLookup(lookup);
44
42
  return parsed[0] as string;
45
43
  };
46
44
 
@@ -6,7 +6,6 @@ export * from './SourceEntity.js';
6
6
  export * from './SourceTable.js';
7
7
  export * from './StorageEngine.js';
8
8
  export * from './StorageProvider.js';
9
- export * from './storage-metrics.js';
10
9
  export * from './WriteCheckpointAPI.js';
11
10
  export * from './BucketStorageFactory.js';
12
11
  export * from './BucketStorageBatch.js';
@@ -7,6 +7,8 @@ import { ErrorCode, logger, ServiceAssertionError, ServiceError } from '@powersy
7
7
  import { BucketParameterQuerier } from '@powersync/service-sync-rules/src/BucketParameterQuerier.js';
8
8
  import { BucketSyncState } from './sync.js';
9
9
  import { SyncContext } from './SyncContext.js';
10
+ import { JSONBig } from '@powersync/service-jsonbig';
11
+ import { hasIntersection } from './util.js';
10
12
 
11
13
  export interface BucketChecksumStateOptions {
12
14
  syncContext: SyncContext;
@@ -268,6 +270,10 @@ export class BucketParameterState {
268
270
  public readonly syncParams: RequestParameters;
269
271
  private readonly querier: BucketParameterQuerier;
270
272
  private readonly staticBuckets: Map<string, BucketDescription>;
273
+ private cachedDynamicBuckets: BucketDescription[] | null = null;
274
+ private cachedDynamicBucketSet: Set<string> | null = null;
275
+
276
+ private readonly lookups: Set<string>;
271
277
 
272
278
  constructor(
273
279
  context: SyncContext,
@@ -282,6 +288,7 @@ export class BucketParameterState {
282
288
 
283
289
  this.querier = syncRules.getBucketParameterQuerier(this.syncParams);
284
290
  this.staticBuckets = new Map<string, BucketDescription>(this.querier.staticBuckets.map((b) => [b.bucket, b]));
291
+ this.lookups = new Set<string>(this.querier.parameterQueryLookups.map((l) => JSONBig.stringify(l)));
285
292
  }
286
293
 
287
294
  async getCheckpointUpdate(checkpoint: storage.StorageCheckpointUpdate): Promise<CheckpointUpdate | null> {
@@ -361,36 +368,61 @@ export class BucketParameterState {
361
368
  const staticBuckets = querier.staticBuckets;
362
369
  const update = checkpoint.update;
363
370
 
364
- let hasChange = false;
365
- if (update.invalidateDataBuckets || update.updatedDataBuckets?.length > 0) {
366
- hasChange = true;
367
- } else if (update.invalidateParameterBuckets) {
368
- hasChange = true;
371
+ let hasParameterChange = false;
372
+ let invalidateDataBuckets = false;
373
+ // If hasParameterChange == true, then invalidateDataBuckets = true
374
+ // If invalidateDataBuckets == true, we ignore updatedBuckets
375
+ let updatedBuckets = new Set<string>();
376
+
377
+ if (update.invalidateDataBuckets) {
378
+ invalidateDataBuckets = true;
379
+ }
380
+
381
+ if (update.invalidateParameterBuckets) {
382
+ hasParameterChange = true;
369
383
  } else {
370
- for (let bucket of update.updatedParameterBucketDefinitions ?? []) {
371
- if (querier.dynamicBucketDefinitions.has(bucket)) {
372
- hasChange = true;
373
- break;
374
- }
384
+ if (hasIntersection(this.lookups, update.updatedParameterLookups)) {
385
+ // This is a very coarse re-check of all queries
386
+ hasParameterChange = true;
375
387
  }
376
388
  }
377
389
 
378
- if (!hasChange) {
379
- return null;
380
- }
390
+ let dynamicBuckets: BucketDescription[];
391
+ if (hasParameterChange || this.cachedDynamicBuckets == null || this.cachedDynamicBucketSet == null) {
392
+ dynamicBuckets = await querier.queryDynamicBucketDescriptions({
393
+ getParameterSets(lookups) {
394
+ return storage.getParameterSets(checkpoint.base.checkpoint, lookups);
395
+ }
396
+ });
397
+ this.cachedDynamicBuckets = dynamicBuckets;
398
+ this.cachedDynamicBucketSet = new Set<string>(dynamicBuckets.map((b) => b.bucket));
399
+ invalidateDataBuckets = true;
400
+ } else {
401
+ dynamicBuckets = this.cachedDynamicBuckets;
381
402
 
382
- const dynamicBuckets = await querier.queryDynamicBucketDescriptions({
383
- getParameterSets(lookups) {
384
- return storage.getParameterSets(checkpoint.base.checkpoint, lookups);
403
+ if (!invalidateDataBuckets) {
404
+ // TODO: Do set intersection instead
405
+ for (let bucket of update.updatedDataBuckets ?? []) {
406
+ if (this.staticBuckets.has(bucket) || this.cachedDynamicBucketSet.has(bucket)) {
407
+ updatedBuckets.add(bucket);
408
+ }
409
+ }
385
410
  }
386
- });
411
+ }
387
412
  const allBuckets = [...staticBuckets, ...dynamicBuckets];
388
413
 
389
- return {
390
- buckets: allBuckets,
391
- // We cannot track individual bucket updates for dynamic lookups yet
392
- updatedBuckets: null
393
- };
414
+ if (invalidateDataBuckets) {
415
+ return {
416
+ buckets: allBuckets,
417
+ // We cannot track individual bucket updates for dynamic lookups yet
418
+ updatedBuckets: null
419
+ };
420
+ } else {
421
+ return {
422
+ buckets: allBuckets,
423
+ updatedBuckets: updatedBuckets
424
+ };
425
+ }
394
426
  }
395
427
  }
396
428
 
@@ -1,6 +1,4 @@
1
- import { MetricsEngine } from '../metrics/MetricsEngine.js';
2
-
3
- import { APIMetric } from '@powersync/service-types';
1
+ import { Metrics } from '../metrics/Metrics.js';
4
2
 
5
3
  /**
6
4
  * Record sync stats per request stream.
@@ -9,19 +7,15 @@ export class RequestTracker {
9
7
  operationsSynced = 0;
10
8
  dataSyncedBytes = 0;
11
9
 
12
- constructor(private metrics: MetricsEngine) {
13
- this.metrics = metrics;
14
- }
15
-
16
10
  addOperationsSynced(operations: number) {
17
11
  this.operationsSynced += operations;
18
12
 
19
- this.metrics.getCounter(APIMetric.OPERATIONS_SYNCED).add(operations);
13
+ Metrics.getInstance().operations_synced_total.add(operations);
20
14
  }
21
15
 
22
16
  addDataSynced(bytes: number) {
23
17
  this.dataSyncedBytes += bytes;
24
18
 
25
- this.metrics.getCounter(APIMetric.DATA_SYNCED_BYTES).add(bytes);
19
+ Metrics.getInstance().data_synced_bytes.add(bytes);
26
20
  }
27
21
  }
package/src/sync/util.ts CHANGED
@@ -153,3 +153,15 @@ export function settledPromise<T>(promise: Promise<T>): Promise<PromiseSettledRe
153
153
  }
154
154
  );
155
155
  }
156
+
157
+ export function hasIntersection<T>(a: Set<T>, b: Set<T>) {
158
+ if (a.size > b.size) {
159
+ [a, b] = [b, a];
160
+ }
161
+ // Now, a is always smaller than b, so iterate over a
162
+ for (let value of a) {
163
+ if (b.has(value)) {
164
+ return true;
165
+ }
166
+ }
167
+ }