@powersync/service-core 1.15.8 → 1.16.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 (38) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/events/EventsEngine.d.ts +14 -0
  3. package/dist/events/EventsEngine.js +33 -0
  4. package/dist/events/EventsEngine.js.map +1 -0
  5. package/dist/routes/configure-fastify.js.map +1 -1
  6. package/dist/routes/endpoints/socket-route.js +15 -2
  7. package/dist/routes/endpoints/socket-route.js.map +1 -1
  8. package/dist/routes/endpoints/sync-stream.js +19 -2
  9. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  10. package/dist/routes/router.d.ts +3 -3
  11. package/dist/storage/BucketStorageFactory.d.ts +2 -0
  12. package/dist/storage/ReportStorage.d.ts +36 -0
  13. package/dist/storage/ReportStorage.js +2 -0
  14. package/dist/storage/ReportStorage.js.map +1 -0
  15. package/dist/storage/StorageEngine.d.ts +2 -2
  16. package/dist/storage/StorageEngine.js.map +1 -1
  17. package/dist/storage/StorageProvider.d.ts +3 -1
  18. package/dist/storage/storage-index.d.ts +1 -0
  19. package/dist/storage/storage-index.js +1 -0
  20. package/dist/storage/storage-index.js.map +1 -1
  21. package/dist/system/ServiceContext.d.ts +3 -0
  22. package/dist/system/ServiceContext.js +10 -1
  23. package/dist/system/ServiceContext.js.map +1 -1
  24. package/package.json +6 -6
  25. package/src/events/EventsEngine.ts +38 -0
  26. package/src/routes/configure-fastify.ts +0 -1
  27. package/src/routes/endpoints/socket-route.ts +16 -3
  28. package/src/routes/endpoints/sync-stream.ts +19 -2
  29. package/src/routes/router.ts +3 -3
  30. package/src/storage/BucketStorageFactory.ts +2 -0
  31. package/src/storage/ReportStorage.ts +39 -0
  32. package/src/storage/StorageEngine.ts +3 -3
  33. package/src/storage/StorageProvider.ts +3 -1
  34. package/src/storage/storage-index.ts +1 -0
  35. package/src/system/ServiceContext.ts +13 -1
  36. package/test/src/routes/mocks.ts +2 -0
  37. package/test/src/routes/stream.test.ts +14 -6
  38. package/tsconfig.tsbuildinfo +1 -1
@@ -7,8 +7,8 @@ import * as util from '../../util/util-index.js';
7
7
 
8
8
  import { authUser } from '../auth.js';
9
9
  import { routeDefinition } from '../router.js';
10
+ import { APIMetric, event_types } from '@powersync/service-types';
10
11
 
11
- import { APIMetric } from '@powersync/service-types';
12
12
  import { maybeCompressResponseStream } from '../compression.js';
13
13
 
14
14
  export enum SyncRoutes {
@@ -25,7 +25,7 @@ export const syncStreamed = routeDefinition({
25
25
  authorize: authUser,
26
26
  validator: schema.createTsCodecValidator(util.StreamingSyncRequest, { allowAdditional: true }),
27
27
  handler: async (payload) => {
28
- const { service_context, logger } = payload.context;
28
+ const { service_context, logger, token_payload } = payload.context;
29
29
  const { routerEngine, storageEngine, metricsEngine, syncContext } = service_context;
30
30
  const headers = payload.request.headers;
31
31
  const userAgent = headers['x-user-agent'] ?? headers['user-agent'];
@@ -44,6 +44,14 @@ export const syncStreamed = routeDefinition({
44
44
  user_id: payload.context.user_id,
45
45
  bson: useBson
46
46
  };
47
+ const sdkData: event_types.ConnectedUserData & event_types.ClientConnectionEventData = {
48
+ client_id: clientId ?? '',
49
+ user_id: payload.context.user_id!,
50
+ user_agent: userAgent as string,
51
+ // At this point the token_payload is guaranteed to be present
52
+ jwt_exp: new Date(token_payload!.exp * 1000),
53
+ connected_at: new Date(streamStart)
54
+ };
47
55
 
48
56
  if (routerEngine.closed) {
49
57
  throw new errors.ServiceError({
@@ -69,6 +77,7 @@ export const syncStreamed = routeDefinition({
69
77
  const tracker = new sync.RequestTracker(metricsEngine);
70
78
  try {
71
79
  metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(1);
80
+ service_context.eventsEngine.emit(event_types.EventsEngineEventType.SDK_CONNECT_EVENT, sdkData);
72
81
  const syncLines = sync.streamResponse({
73
82
  syncContext: syncContext,
74
83
  bucketStorage,
@@ -134,6 +143,10 @@ export const syncStreamed = routeDefinition({
134
143
  }
135
144
  controller.abort();
136
145
  metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(-1);
146
+ service_context.eventsEngine.emit(event_types.EventsEngineEventType.SDK_DISCONNECT_EVENT, {
147
+ ...sdkData,
148
+ disconnected_at: new Date()
149
+ });
137
150
  logger.info(`Sync stream complete`, {
138
151
  ...tracker.getLogMeta(),
139
152
  stream_ms: Date.now() - streamStart,
@@ -144,6 +157,10 @@ export const syncStreamed = routeDefinition({
144
157
  } catch (ex) {
145
158
  controller.abort();
146
159
  metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(-1);
160
+ service_context.eventsEngine.emit(event_types.EventsEngineEventType.SDK_DISCONNECT_EVENT, {
161
+ ...sdkData,
162
+ disconnected_at: new Date()
163
+ });
147
164
  }
148
165
  }
149
166
  });
@@ -1,4 +1,4 @@
1
- import { router, ServiceError, Logger } from '@powersync/lib-services-framework';
1
+ import { Logger, router } from '@powersync/lib-services-framework';
2
2
  import type { JwtPayload } from '../auth/auth-index.js';
3
3
  import { ServiceContext } from '../system/ServiceContext.js';
4
4
  import { RouterEngine } from './RouterEngine.js';
@@ -31,11 +31,11 @@ export type BasicRouterRequest = {
31
31
  hostname: string;
32
32
  };
33
33
 
34
- export type ConextProviderOptions = {
34
+ export type ContextProviderOptions = {
35
35
  logger: Logger;
36
36
  };
37
37
 
38
- export type ContextProvider = (request: BasicRouterRequest, options: ConextProviderOptions) => Promise<Context>;
38
+ export type ContextProvider = (request: BasicRouterRequest, options: ContextProviderOptions) => Promise<Context>;
39
39
 
40
40
  export type RequestEndpoint<
41
41
  I,
@@ -3,6 +3,7 @@ import { ParseSyncRulesOptions, PersistedSyncRules, PersistedSyncRulesContent }
3
3
  import { ReplicationEventPayload } from './ReplicationEventPayload.js';
4
4
  import { ReplicationLock } from './ReplicationLock.js';
5
5
  import { SyncRulesBucketStorage } from './SyncRulesBucketStorage.js';
6
+ import { ReportStorage } from './ReportStorage.js';
6
7
 
7
8
  /**
8
9
  * Represents a configured storage provider.
@@ -164,3 +165,4 @@ export interface TestStorageOptions {
164
165
  doNotClear?: boolean;
165
166
  }
166
167
  export type TestStorageFactory = (options?: TestStorageOptions) => Promise<BucketStorageFactory>;
168
+ export type TestReportStorageFactory = (options?: TestStorageOptions) => Promise<ReportStorage>;
@@ -0,0 +1,39 @@
1
+ import { event_types } from '@powersync/service-types';
2
+
3
+ /**
4
+ * Represents a configured report storage.
5
+ *
6
+ * Report storage is used for storing localized data for the instance.
7
+ * Data can then be used for reporting purposes.
8
+ *
9
+ */
10
+ export interface ReportStorage extends AsyncDisposable {
11
+ /**
12
+ * Report a client connection.
13
+ */
14
+ reportClientConnection(data: event_types.ClientConnectionBucketData): Promise<void>;
15
+ /**
16
+ * Report a client disconnection.
17
+ */
18
+ reportClientDisconnection(data: event_types.ClientDisconnectionEventData): Promise<void>;
19
+ /**
20
+ * Get currently connected clients.
21
+ * This will return any short or long term connected clients.
22
+ * Clients that have no disconnected_at timestamp and that have a valid jwt_exp timestamp are considered connected.
23
+ */
24
+ getConnectedClients(): Promise<event_types.ClientConnectionReportResponse>;
25
+ /**
26
+ * Get a report of client connections over a day, week or month.
27
+ * This is internally used to generate reports over it always returns the previous day, week or month.
28
+ * Usually this is call on the start of the new day, week or month. It will return all unique completed connections
29
+ * as well as uniques currently connected clients.
30
+ */
31
+ getClientConnectionReports(
32
+ data: event_types.ClientConnectionReportRequest
33
+ ): Promise<event_types.ClientConnectionReportResponse>;
34
+ /**
35
+ * Delete old connection data based on a specific date.
36
+ * This is used to clean up old connection data that is no longer needed.
37
+ */
38
+ deleteOldConnectionData(data: event_types.DeleteOldConnectionData): Promise<void>;
39
+ }
@@ -1,7 +1,7 @@
1
1
  import { BaseObserver, logger, ServiceError } from '@powersync/lib-services-framework';
2
2
  import { ResolvedPowerSyncConfig } from '../util/util-index.js';
3
3
  import { BucketStorageFactory } from './BucketStorageFactory.js';
4
- import { ActiveStorage, BucketStorageProvider } from './StorageProvider.js';
4
+ import { ActiveStorage, StorageProvider } from './StorageProvider.js';
5
5
 
6
6
  export type StorageEngineOptions = {
7
7
  configuration: ResolvedPowerSyncConfig;
@@ -14,7 +14,7 @@ export interface StorageEngineListener {
14
14
 
15
15
  export class StorageEngine extends BaseObserver<StorageEngineListener> {
16
16
  // TODO: This will need to revisited when we actually support multiple storage providers.
17
- private storageProviders: Map<string, BucketStorageProvider> = new Map();
17
+ private storageProviders: Map<string, StorageProvider> = new Map();
18
18
  private currentActiveStorage: ActiveStorage | null = null;
19
19
 
20
20
  constructor(private options: StorageEngineOptions) {
@@ -37,7 +37,7 @@ export class StorageEngine extends BaseObserver<StorageEngineListener> {
37
37
  * Register a provider which generates a {@link BucketStorageFactory}
38
38
  * given the matching config specified in the loaded {@link ResolvedPowerSyncConfig}
39
39
  */
40
- registerProvider(provider: BucketStorageProvider) {
40
+ registerProvider(provider: StorageProvider) {
41
41
  this.storageProviders.set(provider.type, provider);
42
42
  }
43
43
 
@@ -1,9 +1,11 @@
1
1
  import { ServiceError } from '@powersync/lib-services-framework';
2
2
  import * as util from '../util/util-index.js';
3
3
  import { BucketStorageFactory } from './BucketStorageFactory.js';
4
+ import { ReportStorage } from './ReportStorage.js';
4
5
 
5
6
  export interface ActiveStorage {
6
7
  storage: BucketStorageFactory;
8
+ reportStorage: ReportStorage;
7
9
  shutDown(): Promise<void>;
8
10
 
9
11
  /**
@@ -22,7 +24,7 @@ export interface GetStorageOptions {
22
24
  /**
23
25
  * Represents a provider that can create a storage instance for a specific storage type from configuration.
24
26
  */
25
- export interface BucketStorageProvider {
27
+ export interface StorageProvider {
26
28
  /**
27
29
  * The storage type that this provider provides.
28
30
  * The type should match the `type` field in the config.
@@ -13,3 +13,4 @@ export * from './BucketStorageBatch.js';
13
13
  export * from './SyncRulesBucketStorage.js';
14
14
  export * from './PersistedSyncRulesContent.js';
15
15
  export * from './ReplicationLock.js';
16
+ export * from './ReportStorage.js';
@@ -1,4 +1,4 @@
1
- import { LifeCycledSystem, MigrationManager, ServiceIdentifier, container } from '@powersync/lib-services-framework';
1
+ import { container, LifeCycledSystem, MigrationManager, ServiceIdentifier } from '@powersync/lib-services-framework';
2
2
 
3
3
  import { framework } from '../index.js';
4
4
  import * as metrics from '../metrics/MetricsEngine.js';
@@ -8,6 +8,7 @@ import * as routes from '../routes/routes-index.js';
8
8
  import * as storage from '../storage/storage-index.js';
9
9
  import { SyncContext } from '../sync/SyncContext.js';
10
10
  import * as utils from '../util/util-index.js';
11
+ import { EventsEngine } from '../events/EventsEngine.js';
11
12
 
12
13
  export interface ServiceContext {
13
14
  configuration: utils.ResolvedPowerSyncConfig;
@@ -19,6 +20,7 @@ export interface ServiceContext {
19
20
  migrations: PowerSyncMigrationManager;
20
21
  syncContext: SyncContext;
21
22
  serviceMode: ServiceContextMode;
23
+ eventsEngine: EventsEngine;
22
24
  }
23
25
 
24
26
  export enum ServiceContextMode {
@@ -45,6 +47,7 @@ export class ServiceContextContainer implements ServiceContext {
45
47
  configuration: utils.ResolvedPowerSyncConfig;
46
48
  lifeCycleEngine: LifeCycledSystem;
47
49
  storageEngine: storage.StorageEngine;
50
+ eventsEngine: EventsEngine;
48
51
  syncContext: SyncContext;
49
52
  routerEngine: routes.RouterEngine;
50
53
  serviceMode: ServiceContextMode;
@@ -66,6 +69,11 @@ export class ServiceContextContainer implements ServiceContext {
66
69
  }
67
70
  });
68
71
 
72
+ this.eventsEngine = new EventsEngine();
73
+ this.lifeCycleEngine.withLifecycle(this.eventsEngine, {
74
+ stop: (emitterEngine) => emitterEngine.shutDown()
75
+ });
76
+
69
77
  this.lifeCycleEngine.withLifecycle(this.storageEngine, {
70
78
  start: (storageEngine) => storageEngine.start(),
71
79
  stop: (storageEngine) => storageEngine.shutDown()
@@ -89,6 +97,10 @@ export class ServiceContextContainer implements ServiceContext {
89
97
  // Migrations should be executed before the system starts
90
98
  start: () => migrationManager[Symbol.asyncDispose]()
91
99
  });
100
+
101
+ this.lifeCycleEngine.withLifecycle(this.eventsEngine, {
102
+ stop: (emitterEngine) => emitterEngine.shutDown()
103
+ });
92
104
  }
93
105
 
94
106
  get replicationEngine(): replication.ReplicationEngine | null {
@@ -11,6 +11,7 @@ import {
11
11
  SyncRulesBucketStorage
12
12
  } from '@/index.js';
13
13
  import { MeterProvider } from '@opentelemetry/sdk-metrics';
14
+ import { EventsEngine } from '@/events/EventsEngine.js';
14
15
 
15
16
  export function mockServiceContext(storage: Partial<SyncRulesBucketStorage> | null) {
16
17
  // This is very incomplete - just enough to get the current tests passing.
@@ -34,6 +35,7 @@ export function mockServiceContext(storage: Partial<SyncRulesBucketStorage> | nu
34
35
  createCoreAPIMetrics(metricsEngine);
35
36
  const service_context: Partial<ServiceContext> = {
36
37
  syncContext: new SyncContext({ maxBuckets: 1, maxDataFetchConcurrency: 1, maxParameterQueryResults: 1 }),
38
+ eventsEngine: new EventsEngine(),
37
39
  routerEngine: {
38
40
  getAPI() {
39
41
  return {
@@ -3,7 +3,7 @@ import { logger, RouterResponse, ServiceError } from '@powersync/lib-services-fr
3
3
  import { SqlSyncRules } from '@powersync/service-sync-rules';
4
4
  import { Readable, Writable } from 'stream';
5
5
  import { pipeline } from 'stream/promises';
6
- import { beforeEach, describe, expect, it, vi } from 'vitest';
6
+ import { describe, expect, it } from 'vitest';
7
7
  import { syncStreamed } from '../../../src/routes/endpoints/sync-stream.js';
8
8
  import { mockServiceContext } from './mocks.js';
9
9
 
@@ -12,7 +12,12 @@ describe('Stream Route', () => {
12
12
  it('handles missing sync rules', async () => {
13
13
  const context: Context = {
14
14
  logger: logger,
15
- service_context: mockServiceContext(null)
15
+ service_context: mockServiceContext(null),
16
+ token_payload: {
17
+ sub: '',
18
+ exp: 0,
19
+ iat: 0
20
+ }
16
21
  };
17
22
 
18
23
  const request: BasicRouterRequest = {
@@ -21,10 +26,13 @@ describe('Stream Route', () => {
21
26
  protocol: 'http'
22
27
  };
23
28
 
24
- const error = (await (syncStreamed.handler({ context, params: {}, request }) as Promise<RouterResponse>).catch(
25
- (e) => e
26
- )) as ServiceError;
27
-
29
+ const error = (await (
30
+ syncStreamed.handler({
31
+ context,
32
+ params: {},
33
+ request
34
+ }) as Promise<RouterResponse>
35
+ ).catch((e) => e)) as ServiceError;
28
36
  expect(error.errorData.status).toEqual(500);
29
37
  expect(error.errorData.code).toEqual('PSYNC_S2302');
30
38
  });