@powersync/service-core 0.0.0-dev-20240708120322 → 0.0.0-dev-20240718134716

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 (48) hide show
  1. package/CHANGELOG.md +42 -2
  2. package/dist/entry/commands/migrate-action.js +12 -4
  3. package/dist/entry/commands/migrate-action.js.map +1 -1
  4. package/dist/metrics/Metrics.d.ts +3 -4
  5. package/dist/metrics/Metrics.js +0 -51
  6. package/dist/metrics/Metrics.js.map +1 -1
  7. package/dist/migrations/migrations.js +8 -0
  8. package/dist/migrations/migrations.js.map +1 -1
  9. package/dist/replication/WalStream.js +8 -6
  10. package/dist/replication/WalStream.js.map +1 -1
  11. package/dist/routes/endpoints/socket-route.js +13 -4
  12. package/dist/routes/endpoints/socket-route.js.map +1 -1
  13. package/dist/routes/endpoints/sync-stream.js +14 -5
  14. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  15. package/dist/routes/route-register.js +1 -0
  16. package/dist/routes/route-register.js.map +1 -1
  17. package/dist/sync/RequestTracker.d.ts +9 -0
  18. package/dist/sync/RequestTracker.js +20 -0
  19. package/dist/sync/RequestTracker.js.map +1 -0
  20. package/dist/sync/sync.d.ts +2 -0
  21. package/dist/sync/sync.js +31 -11
  22. package/dist/sync/sync.js.map +1 -1
  23. package/dist/sync/util.d.ts +2 -1
  24. package/dist/sync/util.js +2 -3
  25. package/dist/sync/util.js.map +1 -1
  26. package/dist/util/config/collectors/config-collector.d.ts +0 -12
  27. package/dist/util/config/collectors/config-collector.js +0 -43
  28. package/dist/util/config/collectors/config-collector.js.map +1 -1
  29. package/dist/util/config/compound-config-collector.d.ts +29 -3
  30. package/dist/util/config/compound-config-collector.js +69 -22
  31. package/dist/util/config/compound-config-collector.js.map +1 -1
  32. package/package.json +4 -6
  33. package/src/entry/commands/migrate-action.ts +12 -4
  34. package/src/metrics/Metrics.ts +2 -67
  35. package/src/migrations/migrations.ts +8 -0
  36. package/src/replication/WalStream.ts +10 -6
  37. package/src/routes/endpoints/socket-route.ts +14 -4
  38. package/src/routes/endpoints/sync-stream.ts +15 -5
  39. package/src/routes/route-register.ts +1 -0
  40. package/src/sync/RequestTracker.ts +21 -0
  41. package/src/sync/sync.ts +41 -11
  42. package/src/sync/util.ts +6 -3
  43. package/src/util/config/collectors/config-collector.ts +0 -48
  44. package/src/util/config/compound-config-collector.ts +87 -23
  45. package/test/src/sync.test.ts +8 -0
  46. package/test/src/util.ts +12 -6
  47. package/test/src/wal_stream.test.ts +16 -21
  48. package/tsconfig.tsbuildinfo +1 -1
@@ -1,3 +1,4 @@
1
+ import * as t from 'ts-codec';
1
2
  import { configFile, normalizeConnection } from '@powersync/service-types';
2
3
  import { ConfigCollector } from './collectors/config-collector.js';
3
4
  import { ResolvedConnection, ResolvedPowerSyncConfig, RunnerConfig, SyncRulesConfig } from './types.js';
@@ -9,7 +10,7 @@ import { Base64SyncRulesCollector } from './sync-rules/impl/base64-sync-rules-co
9
10
  import { InlineSyncRulesCollector } from './sync-rules/impl/inline-sync-rules-collector.js';
10
11
  import { FileSystemSyncRulesCollector } from './sync-rules/impl/filesystem-sync-rules-collector.js';
11
12
  import { FallbackConfigCollector } from './collectors/impl/fallback-config-collector.js';
12
- import { logger } from '@powersync/lib-services-framework';
13
+ import { logger, schema } from '@powersync/lib-services-framework';
13
14
 
14
15
  const POWERSYNC_DEV_KID = 'powersync-dev';
15
16
 
@@ -28,6 +29,12 @@ export type CompoundConfigCollectorOptions = {
28
29
  syncRulesCollectors: SyncRulesCollector[];
29
30
  };
30
31
 
32
+ export type ConfigCollectorGenerics = {
33
+ SERIALIZED: configFile.SerializedPowerSyncConfig;
34
+ DESERIALIZED: configFile.PowerSyncConfig;
35
+ RESOLVED: ResolvedPowerSyncConfig;
36
+ };
37
+
31
38
  const DEFAULT_COLLECTOR_OPTIONS: CompoundConfigCollectorOptions = {
32
39
  configCollectors: [new Base64ConfigCollector(), new FileSystemConfigCollector(), new FallbackConfigCollector()],
33
40
  syncRulesCollectors: [
@@ -37,15 +44,56 @@ const DEFAULT_COLLECTOR_OPTIONS: CompoundConfigCollectorOptions = {
37
44
  ]
38
45
  };
39
46
 
40
- export class CompoundConfigCollector {
47
+ export class CompoundConfigCollector<Generics extends ConfigCollectorGenerics = ConfigCollectorGenerics> {
41
48
  constructor(protected options: CompoundConfigCollectorOptions = DEFAULT_COLLECTOR_OPTIONS) {}
42
49
 
50
+ /**
51
+ * The default ts-codec for validations and decoding
52
+ */
53
+ get codec(): t.AnyCodec {
54
+ return configFile.powerSyncConfig;
55
+ }
56
+
43
57
  /**
44
58
  * Collects and resolves base config
45
59
  */
46
- async collectConfig(runner_config: RunnerConfig = {}): Promise<ResolvedPowerSyncConfig> {
47
- const baseConfig = await this.collectBaseConfig(runner_config);
60
+ async collectConfig(runnerConfig: RunnerConfig = {}): Promise<Generics['RESOLVED']> {
61
+ const baseConfig = await this.collectBaseConfig(runnerConfig);
62
+ const baseResolvedConfig = await this.resolveBaseConfig(baseConfig, runnerConfig);
63
+ return this.resolveConfig(baseConfig, baseResolvedConfig, runnerConfig);
64
+ }
65
+
66
+ /**
67
+ * Collects the base PowerSyncConfig from various registered collectors.
68
+ * @throws if no collector could return a configuration.
69
+ */
70
+ protected async collectBaseConfig(runner_config: RunnerConfig): Promise<Generics['DESERIALIZED']> {
71
+ for (const collector of this.options.configCollectors) {
72
+ try {
73
+ const baseConfig = await collector.collectSerialized(runner_config);
74
+ if (baseConfig) {
75
+ const decoded = this.decode(baseConfig);
76
+ this.validate(decoded);
77
+ return decoded;
78
+ }
79
+ logger.debug(
80
+ `Could not collect PowerSync config with ${collector.name} method. Moving on to next method if available.`
81
+ );
82
+ } catch (ex) {
83
+ // An error in a collector is a hard stop
84
+ throw new Error(`Could not collect config using ${collector.name} method. Caught exception: ${ex}`);
85
+ }
86
+ }
87
+ throw new Error('PowerSyncConfig could not be collected using any of the registered config collectors.');
88
+ }
48
89
 
90
+ /**
91
+ * Performs the resolving of the common (shared) base configuration
92
+ */
93
+ protected async resolveBaseConfig(
94
+ baseConfig: Generics['DESERIALIZED'],
95
+ runnerConfig: RunnerConfig = {}
96
+ ): Promise<ResolvedPowerSyncConfig> {
49
97
  const connections = baseConfig.replication?.connections ?? [];
50
98
  if (connections.length > 1) {
51
99
  throw new Error('Only a single replication connection is supported currently');
@@ -93,7 +141,7 @@ export class CompoundConfigCollector {
93
141
  devKey = await auth.KeySpec.importKey(baseDevKey);
94
142
  }
95
143
 
96
- const sync_rules = await this.collectSyncRules(baseConfig, runner_config);
144
+ const sync_rules = await this.collectSyncRules(baseConfig, runnerConfig);
97
145
 
98
146
  let jwt_audiences: string[] = baseConfig.client_auth?.audience ?? [];
99
147
 
@@ -130,25 +178,17 @@ export class CompoundConfigCollector {
130
178
  }
131
179
 
132
180
  /**
133
- * Collects the base PowerSyncConfig from various registered collectors.
134
- * @throws if no collector could return a configuration.
181
+ * Perform any additional resolving from {@link ResolvedPowerSyncConfig}
182
+ * to the extended {@link Generics['RESOLVED']}
183
+ *
135
184
  */
136
- protected async collectBaseConfig(runner_config: RunnerConfig): Promise<configFile.PowerSyncConfig> {
137
- for (const collector of this.options.configCollectors) {
138
- try {
139
- const baseConfig = await collector.collect(runner_config);
140
- if (baseConfig) {
141
- return baseConfig;
142
- }
143
- logger.debug(
144
- `Could not collect PowerSync config with ${collector.name} method. Moving on to next method if available.`
145
- );
146
- } catch (ex) {
147
- // An error in a collector is a hard stop
148
- throw new Error(`Could not collect config using ${collector.name} method. Caught exception: ${ex}`);
149
- }
150
- }
151
- throw new Error('PowerSyncConfig could not be collected using any of the registered config collectors.');
185
+ protected async resolveConfig(
186
+ baseConfig: Generics['DESERIALIZED'],
187
+ resolvedBaseConfig: ResolvedPowerSyncConfig,
188
+ runnerConfig: RunnerConfig = {}
189
+ ): Promise<Generics['RESOLVED']> {
190
+ // The base version has ResolvedPowerSyncConfig == Generics['RESOLVED']
191
+ return resolvedBaseConfig;
152
192
  }
153
193
 
154
194
  protected async collectSyncRules(
@@ -173,4 +213,28 @@ export class CompoundConfigCollector {
173
213
  present: false
174
214
  };
175
215
  }
216
+
217
+ /**
218
+ * Validates input config
219
+ * ts-codec itself doesn't give great validation errors, so we use json schema for that
220
+ */
221
+ protected validate(config: Generics['DESERIALIZED']) {
222
+ // ts-codec itself doesn't give great validation errors, so we use json schema for that
223
+ const validator = schema
224
+ .parseJSONSchema(t.generateJSONSchema(this.codec, { allowAdditional: true, parsers: [configFile.portParser] }))
225
+ .validator();
226
+
227
+ const valid = validator.validate(config);
228
+ if (!valid.valid) {
229
+ throw new Error(`Failed to validate PowerSync config: ${valid.errors.join(', ')}`);
230
+ }
231
+ }
232
+
233
+ protected decode(encoded: Generics['SERIALIZED']): Generics['DESERIALIZED'] {
234
+ try {
235
+ return this.codec.decode(encoded);
236
+ } catch (ex) {
237
+ throw new Error(`Failed to decode PowerSync config: ${ex}`);
238
+ }
239
+ }
176
240
  }
@@ -9,6 +9,7 @@ import { streamResponse } from '../../src/sync/sync.js';
9
9
  import * as timers from 'timers/promises';
10
10
  import { lsnMakeComparable } from '@powersync/service-jpgwire';
11
11
  import { RequestParameters } from '@powersync/service-sync-rules';
12
+ import { RequestTracker } from '@/sync/RequestTracker.js';
12
13
 
13
14
  describe('sync - mongodb', function () {
14
15
  defineTests(MONGO_STORAGE_FACTORY);
@@ -38,6 +39,8 @@ bucket_definitions:
38
39
  `;
39
40
 
40
41
  function defineTests(factory: StorageFactory) {
42
+ const tracker = new RequestTracker();
43
+
41
44
  test('sync global data', async () => {
42
45
  const f = await factory();
43
46
 
@@ -78,6 +81,7 @@ function defineTests(factory: StorageFactory) {
78
81
  include_checksum: true,
79
82
  raw_data: true
80
83
  },
84
+ tracker,
81
85
  syncParams: new RequestParameters({ sub: '' }, {}),
82
86
  token: { exp: Date.now() / 1000 + 10 } as any
83
87
  });
@@ -118,6 +122,7 @@ function defineTests(factory: StorageFactory) {
118
122
  include_checksum: true,
119
123
  raw_data: false
120
124
  },
125
+ tracker,
121
126
  syncParams: new RequestParameters({ sub: '' }, {}),
122
127
  token: { exp: Date.now() / 1000 + 10 } as any
123
128
  });
@@ -146,6 +151,7 @@ function defineTests(factory: StorageFactory) {
146
151
  include_checksum: true,
147
152
  raw_data: true
148
153
  },
154
+ tracker,
149
155
  syncParams: new RequestParameters({ sub: '' }, {}),
150
156
  token: { exp: 0 } as any
151
157
  });
@@ -172,6 +178,7 @@ function defineTests(factory: StorageFactory) {
172
178
  include_checksum: true,
173
179
  raw_data: true
174
180
  },
181
+ tracker,
175
182
  syncParams: new RequestParameters({ sub: '' }, {}),
176
183
  token: { exp: Date.now() / 1000 + 10 } as any
177
184
  });
@@ -232,6 +239,7 @@ function defineTests(factory: StorageFactory) {
232
239
  include_checksum: true,
233
240
  raw_data: true
234
241
  },
242
+ tracker,
235
243
  syncParams: new RequestParameters({ sub: '' }, {}),
236
244
  token: { exp: exp } as any
237
245
  });
package/test/src/util.ts CHANGED
@@ -7,14 +7,20 @@ import { PowerSyncMongo } from '../../src/storage/mongo/db.js';
7
7
  import { escapeIdentifier } from '../../src/util/pgwire_utils.js';
8
8
  import { env } from './env.js';
9
9
  import { Metrics } from '@/metrics/Metrics.js';
10
+ import { container } from '@powersync/lib-services-framework';
11
+ import { MeterProvider } from '@opentelemetry/sdk-metrics';
12
+ import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
10
13
 
11
14
  // The metrics need to be initialised before they can be used
12
- await Metrics.initialise({
13
- disable_telemetry_sharing: true,
14
- powersync_instance_id: 'test',
15
- internal_metrics_endpoint: 'unused.for.tests.com'
16
- });
17
- Metrics.getInstance().resetCounters();
15
+ const prometheus = new PrometheusExporter();
16
+ const metrics = new Metrics(
17
+ new MeterProvider({
18
+ readers: [prometheus]
19
+ }),
20
+ prometheus
21
+ );
22
+ container.register(Metrics, metrics);
23
+ metrics.resetCounters();
18
24
 
19
25
  export const TEST_URI = env.PG_TEST_URL;
20
26
 
@@ -5,6 +5,7 @@ import { MONGO_STORAGE_FACTORY } from './util.js';
5
5
  import { putOp, removeOp, walStreamTest } from './wal_stream_utils.js';
6
6
  import { pgwireRows } from '@powersync/service-jpgwire';
7
7
  import { Metrics } from '@/metrics/Metrics.js';
8
+ import { container } from '@powersync/lib-services-framework';
8
9
 
9
10
  type StorageFactory = () => Promise<BucketStorageFactory>;
10
11
 
@@ -41,10 +42,9 @@ bucket_definitions:
41
42
 
42
43
  await context.replicateSnapshot();
43
44
 
44
- const startRowCount =
45
- (await Metrics.getInstance().getMetricValueForTests('powersync_rows_replicated_total')) ?? 0;
46
- const startTxCount =
47
- (await Metrics.getInstance().getMetricValueForTests('powersync_transactions_replicated_total')) ?? 0;
45
+ const metrics = container.getImplementation(Metrics);
46
+ const startRowCount = (await metrics.getMetricValueForTests('powersync_rows_replicated_total')) ?? 0;
47
+ const startTxCount = (await metrics.getMetricValueForTests('powersync_transactions_replicated_total')) ?? 0;
48
48
 
49
49
  context.startStreaming();
50
50
 
@@ -59,9 +59,8 @@ bucket_definitions:
59
59
  expect(data).toMatchObject([
60
60
  putOp('test_data', { id: test_id, description: 'test1', num: 1152921504606846976n })
61
61
  ]);
62
- const endRowCount = (await Metrics.getInstance().getMetricValueForTests('powersync_rows_replicated_total')) ?? 0;
63
- const endTxCount =
64
- (await Metrics.getInstance().getMetricValueForTests('powersync_transactions_replicated_total')) ?? 0;
62
+ const endRowCount = (await metrics.getMetricValueForTests('powersync_rows_replicated_total')) ?? 0;
63
+ const endTxCount = (await metrics.getMetricValueForTests('powersync_transactions_replicated_total')) ?? 0;
65
64
  expect(endRowCount - startRowCount).toEqual(1);
66
65
  expect(endTxCount - startTxCount).toEqual(1);
67
66
  })
@@ -83,10 +82,9 @@ bucket_definitions:
83
82
 
84
83
  await context.replicateSnapshot();
85
84
 
86
- const startRowCount =
87
- (await Metrics.getInstance().getMetricValueForTests('powersync_rows_replicated_total')) ?? 0;
88
- const startTxCount =
89
- (await Metrics.getInstance().getMetricValueForTests('powersync_transactions_replicated_total')) ?? 0;
85
+ const metrics = container.getImplementation(Metrics);
86
+ const startRowCount = (await metrics.getMetricValueForTests('powersync_rows_replicated_total')) ?? 0;
87
+ const startTxCount = (await metrics.getMetricValueForTests('powersync_transactions_replicated_total')) ?? 0;
90
88
 
91
89
  context.startStreaming();
92
90
 
@@ -97,9 +95,8 @@ bucket_definitions:
97
95
  const data = await context.getBucketData('global[]');
98
96
 
99
97
  expect(data).toMatchObject([putOp('test_DATA', { id: test_id, description: 'test1' })]);
100
- const endRowCount = (await Metrics.getInstance().getMetricValueForTests('powersync_rows_replicated_total')) ?? 0;
101
- const endTxCount =
102
- (await Metrics.getInstance().getMetricValueForTests('powersync_transactions_replicated_total')) ?? 0;
98
+ const endRowCount = (await metrics.getMetricValueForTests('powersync_rows_replicated_total')) ?? 0;
99
+ const endTxCount = (await metrics.getMetricValueForTests('powersync_transactions_replicated_total')) ?? 0;
103
100
  expect(endRowCount - startRowCount).toEqual(1);
104
101
  expect(endTxCount - startTxCount).toEqual(1);
105
102
  })
@@ -293,10 +290,9 @@ bucket_definitions:
293
290
 
294
291
  await context.replicateSnapshot();
295
292
 
296
- const startRowCount =
297
- (await Metrics.getInstance().getMetricValueForTests('powersync_rows_replicated_total')) ?? 0;
298
- const startTxCount =
299
- (await Metrics.getInstance().getMetricValueForTests('powersync_transactions_replicated_total')) ?? 0;
293
+ const metrics = container.getImplementation(Metrics);
294
+ const startRowCount = (await metrics.getMetricValueForTests('powersync_rows_replicated_total')) ?? 0;
295
+ const startTxCount = (await metrics.getMetricValueForTests('powersync_transactions_replicated_total')) ?? 0;
300
296
 
301
297
  context.startStreaming();
302
298
 
@@ -307,9 +303,8 @@ bucket_definitions:
307
303
  const data = await context.getBucketData('global[]');
308
304
 
309
305
  expect(data).toMatchObject([]);
310
- const endRowCount = (await Metrics.getInstance().getMetricValueForTests('powersync_rows_replicated_total')) ?? 0;
311
- const endTxCount =
312
- (await Metrics.getInstance().getMetricValueForTests('powersync_transactions_replicated_total')) ?? 0;
306
+ const endRowCount = (await metrics.getMetricValueForTests('powersync_rows_replicated_total')) ?? 0;
307
+ const endTxCount = (await metrics.getMetricValueForTests('powersync_transactions_replicated_total')) ?? 0;
313
308
 
314
309
  // There was a transaction, but we should not replicate any actual data
315
310
  expect(endRowCount - startRowCount).toEqual(0);