@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.
- package/CHANGELOG.md +20 -0
- package/dist/events/EventsEngine.d.ts +14 -0
- package/dist/events/EventsEngine.js +33 -0
- package/dist/events/EventsEngine.js.map +1 -0
- package/dist/routes/configure-fastify.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +15 -2
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +19 -2
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/routes/router.d.ts +3 -3
- package/dist/storage/BucketStorageFactory.d.ts +2 -0
- package/dist/storage/ReportStorage.d.ts +36 -0
- package/dist/storage/ReportStorage.js +2 -0
- package/dist/storage/ReportStorage.js.map +1 -0
- package/dist/storage/StorageEngine.d.ts +2 -2
- package/dist/storage/StorageEngine.js.map +1 -1
- package/dist/storage/StorageProvider.d.ts +3 -1
- package/dist/storage/storage-index.d.ts +1 -0
- package/dist/storage/storage-index.js +1 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/system/ServiceContext.d.ts +3 -0
- package/dist/system/ServiceContext.js +10 -1
- package/dist/system/ServiceContext.js.map +1 -1
- package/package.json +6 -6
- package/src/events/EventsEngine.ts +38 -0
- package/src/routes/configure-fastify.ts +0 -1
- package/src/routes/endpoints/socket-route.ts +16 -3
- package/src/routes/endpoints/sync-stream.ts +19 -2
- package/src/routes/router.ts +3 -3
- package/src/storage/BucketStorageFactory.ts +2 -0
- package/src/storage/ReportStorage.ts +39 -0
- package/src/storage/StorageEngine.ts +3 -3
- package/src/storage/StorageProvider.ts +3 -1
- package/src/storage/storage-index.ts +1 -0
- package/src/system/ServiceContext.ts +13 -1
- package/test/src/routes/mocks.ts +2 -0
- package/test/src/routes/stream.test.ts +14 -6
- 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
|
});
|
package/src/routes/router.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
34
|
+
export type ContextProviderOptions = {
|
|
35
35
|
logger: Logger;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
export type ContextProvider = (request: BasicRouterRequest, options:
|
|
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,
|
|
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,
|
|
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:
|
|
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
|
|
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.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LifeCycledSystem, MigrationManager, ServiceIdentifier
|
|
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 {
|
package/test/src/routes/mocks.ts
CHANGED
|
@@ -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 {
|
|
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 (
|
|
25
|
-
(
|
|
26
|
-
|
|
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
|
});
|