@powersync/service-core 1.11.3 → 1.12.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 +46 -0
- package/dist/auth/CachedKeyCollector.js +2 -7
- package/dist/auth/CachedKeyCollector.js.map +1 -1
- package/dist/auth/CompoundKeyCollector.js.map +1 -1
- package/dist/auth/KeyCollector.d.ts +2 -2
- package/dist/auth/KeyStore.js +32 -14
- package/dist/auth/KeyStore.js.map +1 -1
- package/dist/auth/RemoteJWKSCollector.d.ts +1 -0
- package/dist/auth/RemoteJWKSCollector.js +39 -16
- package/dist/auth/RemoteJWKSCollector.js.map +1 -1
- package/dist/auth/auth-index.d.ts +1 -0
- package/dist/auth/auth-index.js +1 -0
- package/dist/auth/auth-index.js.map +1 -1
- package/dist/auth/utils.d.ts +6 -0
- package/dist/auth/utils.js +97 -0
- package/dist/auth/utils.js.map +1 -0
- package/dist/entry/commands/compact-action.js +4 -1
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/entry/commands/migrate-action.js +4 -1
- package/dist/entry/commands/migrate-action.js.map +1 -1
- package/dist/entry/commands/test-connection-action.js +4 -1
- package/dist/entry/commands/test-connection-action.js.map +1 -1
- package/dist/routes/RouterEngine.d.ts +2 -0
- package/dist/routes/RouterEngine.js +15 -10
- package/dist/routes/RouterEngine.js.map +1 -1
- package/dist/routes/auth.d.ts +5 -16
- package/dist/routes/auth.js +6 -4
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +3 -21
- package/dist/routes/configure-fastify.js +3 -6
- package/dist/routes/configure-fastify.js.map +1 -1
- package/dist/routes/configure-rsocket.js +28 -14
- package/dist/routes/configure-rsocket.js.map +1 -1
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/checkpointing.d.ts +4 -28
- package/dist/routes/endpoints/checkpointing.js.map +1 -1
- package/dist/routes/endpoints/route-endpoints-index.d.ts +1 -0
- package/dist/routes/endpoints/route-endpoints-index.js +1 -0
- package/dist/routes/endpoints/route-endpoints-index.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +22 -8
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.d.ts +2 -14
- package/dist/routes/endpoints/sync-stream.js +28 -9
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/routes/route-register.js +10 -6
- package/dist/routes/route-register.js.map +1 -1
- package/dist/routes/router.d.ts +8 -7
- package/dist/routes/router.js.map +1 -1
- package/dist/runner/teardown.js +4 -1
- package/dist/runner/teardown.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +40 -18
- package/dist/sync/BucketChecksumState.js +120 -72
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/RequestTracker.d.ts +22 -1
- package/dist/sync/RequestTracker.js +51 -2
- package/dist/sync/RequestTracker.js.map +1 -1
- package/dist/sync/sync.d.ts +3 -5
- package/dist/sync/sync.js +47 -32
- package/dist/sync/sync.js.map +1 -1
- package/dist/system/ServiceContext.d.ts +19 -4
- package/dist/system/ServiceContext.js +20 -8
- package/dist/system/ServiceContext.js.map +1 -1
- package/dist/util/config/collectors/config-collector.js +4 -33
- package/dist/util/config/collectors/config-collector.js.map +1 -1
- package/dist/util/config/collectors/impl/yaml-env.d.ts +7 -0
- package/dist/util/config/collectors/impl/yaml-env.js +59 -0
- package/dist/util/config/collectors/impl/yaml-env.js.map +1 -0
- package/dist/util/config/compound-config-collector.js +18 -1
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/dist/util/config/types.d.ts +11 -0
- package/package.json +6 -7
- package/src/auth/CachedKeyCollector.ts +4 -6
- package/src/auth/CompoundKeyCollector.ts +2 -1
- package/src/auth/KeyCollector.ts +2 -2
- package/src/auth/KeyStore.ts +45 -20
- package/src/auth/RemoteJWKSCollector.ts +39 -16
- package/src/auth/auth-index.ts +1 -0
- package/src/auth/utils.ts +102 -0
- package/src/entry/commands/compact-action.ts +4 -1
- package/src/entry/commands/migrate-action.ts +4 -1
- package/src/entry/commands/test-connection-action.ts +4 -1
- package/src/routes/RouterEngine.ts +21 -11
- package/src/routes/auth.ts +7 -6
- package/src/routes/configure-fastify.ts +6 -8
- package/src/routes/configure-rsocket.ts +33 -18
- package/src/routes/endpoints/admin.ts +5 -5
- package/src/routes/endpoints/checkpointing.ts +2 -2
- package/src/routes/endpoints/route-endpoints-index.ts +1 -0
- package/src/routes/endpoints/socket-route.ts +27 -11
- package/src/routes/endpoints/sync-rules.ts +4 -4
- package/src/routes/endpoints/sync-stream.ts +34 -11
- package/src/routes/route-register.ts +10 -7
- package/src/routes/router.ts +11 -4
- package/src/runner/teardown.ts +5 -1
- package/src/sync/BucketChecksumState.ts +160 -75
- package/src/sync/RequestTracker.ts +70 -3
- package/src/sync/sync.ts +69 -46
- package/src/system/ServiceContext.ts +31 -12
- package/src/util/config/collectors/config-collector.ts +4 -40
- package/src/util/config/collectors/impl/yaml-env.ts +67 -0
- package/src/util/config/compound-config-collector.ts +22 -5
- package/src/util/config/types.ts +13 -0
- package/test/src/auth.test.ts +29 -11
- package/test/src/config.test.ts +72 -0
- package/test/src/sync/BucketChecksumState.test.ts +32 -18
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -2,15 +2,8 @@ import { logger } from '@powersync/lib-services-framework';
|
|
|
2
2
|
|
|
3
3
|
import * as api from '../api/api-index.js';
|
|
4
4
|
|
|
5
|
-
import { ADMIN_ROUTES } from './endpoints/admin.js';
|
|
6
|
-
import { CHECKPOINT_ROUTES } from './endpoints/checkpointing.js';
|
|
7
|
-
import { PROBES_ROUTES } from './endpoints/probes.js';
|
|
8
|
-
import { syncStreamReactive } from './endpoints/socket-route.js';
|
|
9
|
-
import { SYNC_RULES_ROUTES } from './endpoints/sync-rules.js';
|
|
10
|
-
import { SYNC_STREAM_ROUTES } from './endpoints/sync-stream.js';
|
|
11
5
|
import { SocketRouteGenerator } from './router-socket.js';
|
|
12
6
|
import { RouteDefinition } from './router.js';
|
|
13
|
-
import { SyncContext } from '../sync/SyncContext.js';
|
|
14
7
|
|
|
15
8
|
export type RouterSetupResponse = {
|
|
16
9
|
onShutdown: () => Promise<void>;
|
|
@@ -47,14 +40,25 @@ export class RouterEngine {
|
|
|
47
40
|
this.cleanupHandler = null;
|
|
48
41
|
this.closed = false;
|
|
49
42
|
|
|
50
|
-
// Default routes
|
|
51
43
|
this.routes = {
|
|
52
|
-
api_routes: [
|
|
53
|
-
stream_routes: [
|
|
54
|
-
socket_routes: [
|
|
44
|
+
api_routes: [],
|
|
45
|
+
stream_routes: [],
|
|
46
|
+
socket_routes: []
|
|
55
47
|
};
|
|
56
48
|
}
|
|
57
49
|
|
|
50
|
+
public registerRoutes(routes: Partial<RouterEngineRoutes>) {
|
|
51
|
+
this.routes.api_routes.push(...(routes.api_routes ?? []));
|
|
52
|
+
this.routes.stream_routes.push(...(routes.stream_routes ?? []));
|
|
53
|
+
this.routes.socket_routes.push(...(routes.socket_routes ?? []));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public get hasRoutes() {
|
|
57
|
+
return (
|
|
58
|
+
this.routes.api_routes.length > 0 || this.routes.stream_routes.length > 0 || this.routes.socket_routes.length > 0
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
58
62
|
public registerAPI(api: api.RouteAPI) {
|
|
59
63
|
if (this.api) {
|
|
60
64
|
logger.warn('A RouteAPI has already been registered. Overriding existing implementation');
|
|
@@ -75,6 +79,12 @@ export class RouterEngine {
|
|
|
75
79
|
*/
|
|
76
80
|
async start(setup: RouterSetup) {
|
|
77
81
|
logger.info('Starting Router Engine...');
|
|
82
|
+
|
|
83
|
+
if (!this.hasRoutes) {
|
|
84
|
+
logger.info('Router Engine will not start an HTTP server as no routes have been registered.');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
78
88
|
const { onShutdown } = await setup(this.routes);
|
|
79
89
|
this.cleanupHandler = onShutdown;
|
|
80
90
|
logger.info('Successfully started Router Engine.');
|
package/src/routes/auth.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as auth from '../auth/auth-index.js';
|
|
|
4
4
|
import { ServiceContext } from '../system/ServiceContext.js';
|
|
5
5
|
import * as util from '../util/util-index.js';
|
|
6
6
|
import { BasicRouterRequest, Context, RequestEndpointHandlerPayload } from './router.js';
|
|
7
|
+
import { AuthorizationError, AuthorizationResponse, ErrorCode, ServiceError } from '@powersync/lib-services-framework';
|
|
7
8
|
|
|
8
9
|
export function endpoint(req: BasicRouterRequest) {
|
|
9
10
|
const protocol = req.headers['x-forwarded-proto'] ?? req.protocol;
|
|
@@ -95,25 +96,25 @@ export function getTokenFromHeader(authHeader: string = ''): string | null {
|
|
|
95
96
|
return token ?? null;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
export const authUser = async (payload: RequestEndpointHandlerPayload) => {
|
|
99
|
+
export const authUser = async (payload: RequestEndpointHandlerPayload): Promise<AuthorizationResponse> => {
|
|
99
100
|
return authorizeUser(payload.context, payload.request.headers.authorization as string);
|
|
100
101
|
};
|
|
101
102
|
|
|
102
|
-
export async function authorizeUser(context: Context, authHeader: string = '') {
|
|
103
|
+
export async function authorizeUser(context: Context, authHeader: string = ''): Promise<AuthorizationResponse> {
|
|
103
104
|
const token = getTokenFromHeader(authHeader);
|
|
104
105
|
if (token == null) {
|
|
105
106
|
return {
|
|
106
107
|
authorized: false,
|
|
107
|
-
|
|
108
|
+
error: new AuthorizationError(ErrorCode.PSYNC_S2106, 'Authentication required')
|
|
108
109
|
};
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
const { context: tokenContext,
|
|
112
|
+
const { context: tokenContext, tokenError } = await generateContext(context.service_context, token);
|
|
112
113
|
|
|
113
114
|
if (!tokenContext) {
|
|
114
115
|
return {
|
|
115
116
|
authorized: false,
|
|
116
|
-
|
|
117
|
+
error: tokenError
|
|
117
118
|
};
|
|
118
119
|
}
|
|
119
120
|
|
|
@@ -140,7 +141,7 @@ export async function generateContext(serviceContext: ServiceContext, token: str
|
|
|
140
141
|
} catch (err) {
|
|
141
142
|
return {
|
|
142
143
|
context: null,
|
|
143
|
-
|
|
144
|
+
tokenError: auth.mapAuthError(err, token)
|
|
144
145
|
};
|
|
145
146
|
}
|
|
146
147
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type fastify from 'fastify';
|
|
2
|
+
import * as uuid from 'uuid';
|
|
3
|
+
|
|
2
4
|
import { registerFastifyRoutes } from './route-register.js';
|
|
3
5
|
|
|
4
6
|
import * as system from '../system/system-index.js';
|
|
@@ -9,7 +11,7 @@ import { PROBES_ROUTES } from './endpoints/probes.js';
|
|
|
9
11
|
import { SYNC_RULES_ROUTES } from './endpoints/sync-rules.js';
|
|
10
12
|
import { SYNC_STREAM_ROUTES } from './endpoints/sync-stream.js';
|
|
11
13
|
import { createRequestQueueHook, CreateRequestQueueParams } from './hooks.js';
|
|
12
|
-
import {
|
|
14
|
+
import { ContextProvider, RouteDefinition } from './router.js';
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* A list of route definitions to be registered as endpoints.
|
|
@@ -58,15 +60,11 @@ export const DEFAULT_ROUTE_OPTIONS = {
|
|
|
58
60
|
export function configureFastifyServer(server: fastify.FastifyInstance, options: FastifyServerConfig) {
|
|
59
61
|
const { service_context, routes = DEFAULT_ROUTE_OPTIONS } = options;
|
|
60
62
|
|
|
61
|
-
const generateContext = async () => {
|
|
62
|
-
const { routerEngine } = service_context;
|
|
63
|
-
if (!routerEngine) {
|
|
64
|
-
throw new Error(`RouterEngine has not been registered`);
|
|
65
|
-
}
|
|
66
|
-
|
|
63
|
+
const generateContext: ContextProvider = async (request, options) => {
|
|
67
64
|
return {
|
|
68
65
|
user_id: undefined,
|
|
69
|
-
service_context: service_context
|
|
66
|
+
service_context: service_context,
|
|
67
|
+
logger: options.logger
|
|
70
68
|
};
|
|
71
69
|
};
|
|
72
70
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { deserialize } from 'bson';
|
|
2
2
|
import * as http from 'http';
|
|
3
|
+
import * as uuid from 'uuid';
|
|
3
4
|
|
|
4
|
-
import { errors, logger } from '@powersync/lib-services-framework';
|
|
5
|
-
import { ReactiveSocketRouter, RSocketRequestMeta } from '@powersync/service-rsocket-router';
|
|
5
|
+
import { ErrorCode, errors, logger } from '@powersync/lib-services-framework';
|
|
6
|
+
import { ReactiveSocketRouter, RSocketRequestMeta, TypedBuffer } from '@powersync/service-rsocket-router';
|
|
6
7
|
|
|
7
8
|
import { ServiceContext } from '../system/ServiceContext.js';
|
|
8
9
|
import { generateContext, getTokenFromHeader } from './auth.js';
|
|
@@ -22,44 +23,58 @@ export function configureRSocket(router: ReactiveSocketRouter<Context>, options:
|
|
|
22
23
|
const { route_generators = DEFAULT_SOCKET_ROUTES, server, service_context } = options;
|
|
23
24
|
|
|
24
25
|
router.applyWebSocketEndpoints(server, {
|
|
25
|
-
contextProvider: async (data:
|
|
26
|
-
const
|
|
26
|
+
contextProvider: async (data: TypedBuffer): Promise<Context & { token: string }> => {
|
|
27
|
+
const connectionLogger = logger.child({
|
|
28
|
+
// timestamp-based uuid - useful for requests
|
|
29
|
+
rid: `s/${uuid.v7()}`
|
|
30
|
+
});
|
|
31
|
+
const { token, user_agent } = RSocketContextMeta.decode(decodeTyped(data) as any);
|
|
27
32
|
|
|
28
33
|
if (!token) {
|
|
29
|
-
throw new errors.AuthorizationError('No token provided');
|
|
34
|
+
throw new errors.AuthorizationError(ErrorCode.PSYNC_S2106, 'No token provided');
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
try {
|
|
33
38
|
const extracted_token = getTokenFromHeader(token);
|
|
34
39
|
if (extracted_token != null) {
|
|
35
|
-
const { context,
|
|
40
|
+
const { context, tokenError } = await generateContext(options.service_context, extracted_token);
|
|
36
41
|
if (context?.token_payload == null) {
|
|
37
|
-
throw new errors.AuthorizationError(
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (!service_context.routerEngine) {
|
|
41
|
-
throw new Error(`RouterEngine has not been registered`);
|
|
42
|
+
throw new errors.AuthorizationError(ErrorCode.PSYNC_S2106, 'Authentication required');
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
return {
|
|
45
46
|
token,
|
|
46
47
|
user_agent,
|
|
47
48
|
...context,
|
|
48
|
-
|
|
49
|
-
service_context: service_context as RouterServiceContext
|
|
49
|
+
token_error: tokenError,
|
|
50
|
+
service_context: service_context as RouterServiceContext,
|
|
51
|
+
logger: connectionLogger
|
|
50
52
|
};
|
|
51
53
|
} else {
|
|
52
|
-
|
|
54
|
+
// Token field is present, but did not contain a token.
|
|
55
|
+
throw new errors.AuthorizationError(ErrorCode.PSYNC_S2106, 'No valid token provided');
|
|
53
56
|
}
|
|
54
57
|
} catch (ex) {
|
|
55
|
-
|
|
58
|
+
connectionLogger.error(ex);
|
|
56
59
|
throw ex;
|
|
57
60
|
}
|
|
58
61
|
},
|
|
59
62
|
endpoints: route_generators.map((generator) => generator(router)),
|
|
60
|
-
metaDecoder: async (meta:
|
|
61
|
-
return RSocketRequestMeta.decode(
|
|
63
|
+
metaDecoder: async (meta: TypedBuffer) => {
|
|
64
|
+
return RSocketRequestMeta.decode(decodeTyped(meta) as any);
|
|
62
65
|
},
|
|
63
|
-
payloadDecoder: async (rawData?:
|
|
66
|
+
payloadDecoder: async (rawData?: TypedBuffer) => rawData && decodeTyped(rawData)
|
|
64
67
|
});
|
|
65
68
|
}
|
|
69
|
+
|
|
70
|
+
function decodeTyped(data: TypedBuffer) {
|
|
71
|
+
switch (data.mimeType) {
|
|
72
|
+
case 'application/json':
|
|
73
|
+
const decoder = new TextDecoder();
|
|
74
|
+
return JSON.parse(decoder.decode(data.contents));
|
|
75
|
+
case 'application/bson':
|
|
76
|
+
return deserialize(data.contents);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new errors.UnsupportedMediaType(`Expected JSON or BSON request, got ${data.mimeType}`);
|
|
80
|
+
}
|
|
@@ -19,7 +19,7 @@ export const executeSql = routeDefinition({
|
|
|
19
19
|
}
|
|
20
20
|
} = payload;
|
|
21
21
|
|
|
22
|
-
const apiHandler = payload.context.service_context.routerEngine
|
|
22
|
+
const apiHandler = payload.context.service_context.routerEngine.getAPI();
|
|
23
23
|
|
|
24
24
|
const sourceConfig = await apiHandler.getSourceConfig();
|
|
25
25
|
if (!sourceConfig.debug_api) {
|
|
@@ -47,7 +47,7 @@ export const diagnostics = routeDefinition({
|
|
|
47
47
|
const { service_context } = context;
|
|
48
48
|
const include_content = payload.params.sync_rules_content ?? false;
|
|
49
49
|
|
|
50
|
-
const apiHandler = service_context.routerEngine
|
|
50
|
+
const apiHandler = service_context.routerEngine.getAPI();
|
|
51
51
|
|
|
52
52
|
const status = await apiHandler.getConnectionStatus();
|
|
53
53
|
if (!status) {
|
|
@@ -94,7 +94,7 @@ export const getSchema = routeDefinition({
|
|
|
94
94
|
authorize: authApi,
|
|
95
95
|
validator: schema.createTsCodecValidator(internal_routes.GetSchemaRequest, { allowAdditional: true }),
|
|
96
96
|
handler: async (payload) => {
|
|
97
|
-
const apiHandler = payload.context.service_context.routerEngine
|
|
97
|
+
const apiHandler = payload.context.service_context.routerEngine.getAPI();
|
|
98
98
|
|
|
99
99
|
return internal_routes.GetSchemaResponse.encode(await api.getConnectionsSchema(apiHandler));
|
|
100
100
|
}
|
|
@@ -112,7 +112,7 @@ export const reprocess = routeDefinition({
|
|
|
112
112
|
const {
|
|
113
113
|
storageEngine: { activeBucketStorage }
|
|
114
114
|
} = service_context;
|
|
115
|
-
const apiHandler = service_context.routerEngine
|
|
115
|
+
const apiHandler = service_context.routerEngine.getAPI();
|
|
116
116
|
const next = await activeBucketStorage.getNextSyncRules(apiHandler.getParseSyncRulesOptions());
|
|
117
117
|
if (next != null) {
|
|
118
118
|
throw new Error(`Busy processing sync rules - cannot reprocess`);
|
|
@@ -159,7 +159,7 @@ export const validate = routeDefinition({
|
|
|
159
159
|
context: { service_context }
|
|
160
160
|
} = payload;
|
|
161
161
|
const content = payload.params.sync_rules;
|
|
162
|
-
const apiHandler = service_context.routerEngine
|
|
162
|
+
const apiHandler = service_context.routerEngine.getAPI();
|
|
163
163
|
|
|
164
164
|
const schemaData = await api.getConnectionsSchema(apiHandler);
|
|
165
165
|
const schema = new StaticSchema(schemaData.connections);
|
|
@@ -18,7 +18,7 @@ export const writeCheckpoint = routeDefinition({
|
|
|
18
18
|
const {
|
|
19
19
|
context: { service_context }
|
|
20
20
|
} = payload;
|
|
21
|
-
const apiHandler = service_context.routerEngine
|
|
21
|
+
const apiHandler = service_context.routerEngine.getAPI();
|
|
22
22
|
|
|
23
23
|
// This old API needs a persisted checkpoint id.
|
|
24
24
|
// Since we don't use LSNs anymore, the only way to get that is to wait.
|
|
@@ -54,7 +54,7 @@ export const writeCheckpoint2 = routeDefinition({
|
|
|
54
54
|
handler: async (payload) => {
|
|
55
55
|
const { user_id, service_context } = payload.context;
|
|
56
56
|
|
|
57
|
-
const apiHandler = service_context.routerEngine
|
|
57
|
+
const apiHandler = service_context.routerEngine.getAPI();
|
|
58
58
|
|
|
59
59
|
const { replicationHead, writeCheckpoint } = await util.createWriteCheckpoint({
|
|
60
60
|
userId: user_id,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ErrorCode, errors,
|
|
1
|
+
import { ErrorCode, errors, schema } from '@powersync/lib-services-framework';
|
|
2
2
|
import { RequestParameters } from '@powersync/service-sync-rules';
|
|
3
3
|
import { serialize } from 'bson';
|
|
4
4
|
|
|
@@ -13,12 +13,26 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
13
13
|
router.reactiveStream<util.StreamingSyncRequest, any>(SyncRoutes.STREAM, {
|
|
14
14
|
validator: schema.createTsCodecValidator(util.StreamingSyncRequest, { allowAdditional: true }),
|
|
15
15
|
handler: async ({ context, params, responder, observer, initialN, signal: upstreamSignal }) => {
|
|
16
|
-
const { service_context } = context;
|
|
16
|
+
const { service_context, logger } = context;
|
|
17
17
|
const { routerEngine, metricsEngine, syncContext } = service_context;
|
|
18
18
|
|
|
19
|
+
logger.defaultMeta = {
|
|
20
|
+
...logger.defaultMeta,
|
|
21
|
+
user_id: context.token_payload?.sub,
|
|
22
|
+
client_id: params.client_id,
|
|
23
|
+
user_agent: context.user_agent
|
|
24
|
+
};
|
|
25
|
+
const streamStart = Date.now();
|
|
26
|
+
|
|
27
|
+
// Best effort guess on why the stream was closed.
|
|
28
|
+
// We use the `??=` operator everywhere, so that we catch the first relevant
|
|
29
|
+
// event, which is usually the most specific.
|
|
30
|
+
let closeReason: string | undefined = undefined;
|
|
31
|
+
|
|
19
32
|
// Create our own controller that we can abort directly
|
|
20
33
|
const controller = new AbortController();
|
|
21
34
|
upstreamSignal.addEventListener('abort', () => {
|
|
35
|
+
closeReason ??= 'client closing stream';
|
|
22
36
|
controller.abort();
|
|
23
37
|
});
|
|
24
38
|
if (upstreamSignal.aborted) {
|
|
@@ -33,7 +47,7 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
33
47
|
}
|
|
34
48
|
});
|
|
35
49
|
|
|
36
|
-
if (routerEngine
|
|
50
|
+
if (routerEngine.closed) {
|
|
37
51
|
responder.onError(
|
|
38
52
|
new errors.ServiceError({
|
|
39
53
|
status: 503,
|
|
@@ -64,9 +78,10 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
64
78
|
return;
|
|
65
79
|
}
|
|
66
80
|
|
|
67
|
-
const syncRules = bucketStorage.getParsedSyncRules(routerEngine
|
|
81
|
+
const syncRules = bucketStorage.getParsedSyncRules(routerEngine.getAPI().getParseSyncRulesOptions());
|
|
68
82
|
|
|
69
|
-
const removeStopHandler = routerEngine
|
|
83
|
+
const removeStopHandler = routerEngine.addStopHandler(() => {
|
|
84
|
+
closeReason ??= 'process shutdown';
|
|
70
85
|
controller.abort();
|
|
71
86
|
});
|
|
72
87
|
|
|
@@ -88,7 +103,8 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
88
103
|
keep_alive: false
|
|
89
104
|
},
|
|
90
105
|
tracker,
|
|
91
|
-
signal
|
|
106
|
+
signal,
|
|
107
|
+
logger
|
|
92
108
|
})) {
|
|
93
109
|
if (signal.aborted) {
|
|
94
110
|
break;
|
|
@@ -131,22 +147,22 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
131
147
|
});
|
|
132
148
|
}
|
|
133
149
|
}
|
|
150
|
+
closeReason ??= 'service closing stream';
|
|
134
151
|
} catch (ex) {
|
|
135
152
|
// Convert to our standard form before responding.
|
|
136
153
|
// This ensures the error can be serialized.
|
|
137
154
|
const error = new errors.InternalServerError(ex);
|
|
138
155
|
logger.error('Sync stream error', error);
|
|
156
|
+
closeReason ??= 'stream error';
|
|
139
157
|
responder.onError(error);
|
|
140
158
|
} finally {
|
|
141
159
|
responder.onComplete();
|
|
142
160
|
removeStopHandler();
|
|
143
161
|
disposer();
|
|
144
162
|
logger.info(`Sync stream complete`, {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
operations_synced: tracker.operationsSynced,
|
|
149
|
-
data_synced_bytes: tracker.dataSyncedBytes
|
|
163
|
+
...tracker.getLogMeta(),
|
|
164
|
+
stream_ms: Date.now() - streamStart,
|
|
165
|
+
close_reason: closeReason ?? 'unknown'
|
|
150
166
|
});
|
|
151
167
|
metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(-1);
|
|
152
168
|
}
|
|
@@ -53,7 +53,7 @@ export const deploySyncRules = routeDefinition({
|
|
|
53
53
|
const content = payload.params.content;
|
|
54
54
|
|
|
55
55
|
try {
|
|
56
|
-
const apiHandler = service_context.routerEngine
|
|
56
|
+
const apiHandler = service_context.routerEngine.getAPI();
|
|
57
57
|
SqlSyncRules.fromYaml(payload.params.content, {
|
|
58
58
|
...apiHandler.getParseSyncRulesOptions(),
|
|
59
59
|
// We don't do any schema-level validation at this point
|
|
@@ -94,7 +94,7 @@ export const validateSyncRules = routeDefinition({
|
|
|
94
94
|
handler: async (payload) => {
|
|
95
95
|
const content = payload.params.content;
|
|
96
96
|
const { service_context } = payload.context;
|
|
97
|
-
const apiHandler = service_context.routerEngine
|
|
97
|
+
const apiHandler = service_context.routerEngine.getAPI();
|
|
98
98
|
|
|
99
99
|
const info = await debugSyncRules(apiHandler, content);
|
|
100
100
|
|
|
@@ -121,7 +121,7 @@ export const currentSyncRules = routeDefinition({
|
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
const apiHandler = service_context.routerEngine
|
|
124
|
+
const apiHandler = service_context.routerEngine.getAPI();
|
|
125
125
|
const info = await debugSyncRules(apiHandler, sync_rules.sync_rules_content);
|
|
126
126
|
const next = await activeBucketStorage.getNextSyncRulesContent();
|
|
127
127
|
|
|
@@ -158,7 +158,7 @@ export const reprocessSyncRules = routeDefinition({
|
|
|
158
158
|
const {
|
|
159
159
|
storageEngine: { activeBucketStorage }
|
|
160
160
|
} = payload.context.service_context;
|
|
161
|
-
const apiHandler = payload.context.service_context.routerEngine
|
|
161
|
+
const apiHandler = payload.context.service_context.routerEngine.getAPI();
|
|
162
162
|
const sync_rules = await activeBucketStorage.getActiveSyncRules(apiHandler.getParseSyncRulesOptions());
|
|
163
163
|
if (sync_rules == null) {
|
|
164
164
|
throw new errors.ServiceError({
|
|
@@ -20,13 +20,21 @@ export const syncStreamed = routeDefinition({
|
|
|
20
20
|
authorize: authUser,
|
|
21
21
|
validator: schema.createTsCodecValidator(util.StreamingSyncRequest, { allowAdditional: true }),
|
|
22
22
|
handler: async (payload) => {
|
|
23
|
-
const { service_context } = payload.context;
|
|
23
|
+
const { service_context, logger } = payload.context;
|
|
24
24
|
const { routerEngine, storageEngine, metricsEngine, syncContext } = service_context;
|
|
25
25
|
const headers = payload.request.headers;
|
|
26
26
|
const userAgent = headers['x-user-agent'] ?? headers['user-agent'];
|
|
27
27
|
const clientId = payload.params.client_id;
|
|
28
|
+
const streamStart = Date.now();
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
logger.defaultMeta = {
|
|
31
|
+
...logger.defaultMeta,
|
|
32
|
+
user_agent: userAgent,
|
|
33
|
+
client_id: clientId,
|
|
34
|
+
user_id: payload.context.user_id
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (routerEngine.closed) {
|
|
30
38
|
throw new errors.ServiceError({
|
|
31
39
|
status: 503,
|
|
32
40
|
code: ErrorCode.PSYNC_S2003,
|
|
@@ -47,7 +55,7 @@ export const syncStreamed = routeDefinition({
|
|
|
47
55
|
});
|
|
48
56
|
}
|
|
49
57
|
|
|
50
|
-
const syncRules = bucketStorage.getParsedSyncRules(routerEngine
|
|
58
|
+
const syncRules = bucketStorage.getParsedSyncRules(routerEngine.getAPI().getParseSyncRulesOptions());
|
|
51
59
|
|
|
52
60
|
const controller = new AbortController();
|
|
53
61
|
const tracker = new sync.RequestTracker(metricsEngine);
|
|
@@ -64,7 +72,8 @@ export const syncStreamed = routeDefinition({
|
|
|
64
72
|
syncParams,
|
|
65
73
|
token: payload.context.token_payload!,
|
|
66
74
|
tracker,
|
|
67
|
-
signal: controller.signal
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
logger
|
|
68
77
|
})
|
|
69
78
|
),
|
|
70
79
|
tracker
|
|
@@ -72,16 +81,29 @@ export const syncStreamed = routeDefinition({
|
|
|
72
81
|
{ objectMode: false, highWaterMark: 16 * 1024 }
|
|
73
82
|
);
|
|
74
83
|
|
|
75
|
-
|
|
84
|
+
// Best effort guess on why the stream was closed.
|
|
85
|
+
// We use the `??=` operator everywhere, so that we catch the first relevant
|
|
86
|
+
// event, which is usually the most specific.
|
|
87
|
+
let closeReason: string | undefined = undefined;
|
|
88
|
+
|
|
89
|
+
const deregister = routerEngine.addStopHandler(() => {
|
|
76
90
|
// This error is not currently propagated to the client
|
|
77
91
|
controller.abort();
|
|
92
|
+
closeReason ??= 'process shutdown';
|
|
78
93
|
stream.destroy(new Error('Shutting down system'));
|
|
79
94
|
});
|
|
95
|
+
|
|
96
|
+
stream.on('end', () => {
|
|
97
|
+
// Auth failure or switch to new sync rules
|
|
98
|
+
closeReason ??= 'service closing stream';
|
|
99
|
+
});
|
|
100
|
+
|
|
80
101
|
stream.on('close', () => {
|
|
81
102
|
deregister();
|
|
82
103
|
});
|
|
83
104
|
|
|
84
105
|
stream.on('error', (error) => {
|
|
106
|
+
closeReason ??= 'stream error';
|
|
85
107
|
controller.abort();
|
|
86
108
|
// Note: This appears as a 200 response in the logs.
|
|
87
109
|
if (error.message != 'Shutting down system') {
|
|
@@ -95,15 +117,16 @@ export const syncStreamed = routeDefinition({
|
|
|
95
117
|
'Content-Type': 'application/x-ndjson'
|
|
96
118
|
},
|
|
97
119
|
data: stream,
|
|
98
|
-
afterSend: async () => {
|
|
120
|
+
afterSend: async (details) => {
|
|
121
|
+
if (details.clientClosed) {
|
|
122
|
+
closeReason ??= 'client closing stream';
|
|
123
|
+
}
|
|
99
124
|
controller.abort();
|
|
100
125
|
metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(-1);
|
|
101
126
|
logger.info(`Sync stream complete`, {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
operations_synced: tracker.operationsSynced,
|
|
106
|
-
data_synced_bytes: tracker.dataSyncedBytes
|
|
127
|
+
...tracker.getLogMeta(),
|
|
128
|
+
stream_ms: Date.now() - streamStart,
|
|
129
|
+
close_reason: closeReason ?? 'unknown'
|
|
107
130
|
});
|
|
108
131
|
}
|
|
109
132
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type fastify from 'fastify';
|
|
2
|
+
import * as uuid from 'uuid';
|
|
2
3
|
|
|
3
4
|
import { errors, HTTPMethod, logger, router } from '@powersync/lib-services-framework';
|
|
4
5
|
import { Context, ContextProvider, RequestEndpoint, RequestEndpointHandlerPayload } from './router.js';
|
|
@@ -25,9 +26,12 @@ export function registerFastifyRoutes(
|
|
|
25
26
|
handler: async (request, reply) => {
|
|
26
27
|
const startTime = new Date();
|
|
27
28
|
let response: router.RouterResponse;
|
|
29
|
+
const requestLogger = logger.child({
|
|
30
|
+
route: e.path,
|
|
31
|
+
rid: `h/${uuid.v7()}`
|
|
32
|
+
});
|
|
28
33
|
try {
|
|
29
|
-
const context = await contextProvider(request);
|
|
30
|
-
|
|
34
|
+
const context = await contextProvider(request, { logger: requestLogger });
|
|
31
35
|
let combined = {
|
|
32
36
|
...(request.params as any),
|
|
33
37
|
...(request.query as any)
|
|
@@ -63,7 +67,7 @@ export function registerFastifyRoutes(
|
|
|
63
67
|
}
|
|
64
68
|
} catch (ex) {
|
|
65
69
|
const serviceError = errors.asServiceError(ex);
|
|
66
|
-
|
|
70
|
+
requestLogger.error(`Request failed`, serviceError);
|
|
67
71
|
|
|
68
72
|
response = new router.RouterResponse({
|
|
69
73
|
status: serviceError.errorData.status || 500,
|
|
@@ -83,13 +87,12 @@ export function registerFastifyRoutes(
|
|
|
83
87
|
try {
|
|
84
88
|
await reply.send(response.data);
|
|
85
89
|
} finally {
|
|
86
|
-
await response.afterSend?.();
|
|
87
|
-
|
|
90
|
+
await response.afterSend?.({ clientClosed: request.socket.closed });
|
|
91
|
+
requestLogger.info(`${e.method} ${request.url}`, {
|
|
88
92
|
duration_ms: Math.round(new Date().valueOf() - startTime.valueOf() + Number.EPSILON),
|
|
89
93
|
status: response.status,
|
|
90
94
|
method: e.method,
|
|
91
|
-
path: request.url
|
|
92
|
-
route: e.path
|
|
95
|
+
path: request.url
|
|
93
96
|
});
|
|
94
97
|
}
|
|
95
98
|
}
|
package/src/routes/router.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { router } from '@powersync/lib-services-framework';
|
|
1
|
+
import { router, ServiceError, Logger } 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';
|
|
@@ -6,7 +6,8 @@ import { RouterEngine } from './RouterEngine.js';
|
|
|
6
6
|
/**
|
|
7
7
|
* The {@link RouterEngine} must be provided for these routes
|
|
8
8
|
*/
|
|
9
|
-
export type RouterServiceContext = ServiceContext
|
|
9
|
+
export type RouterServiceContext = ServiceContext;
|
|
10
|
+
|
|
10
11
|
/**
|
|
11
12
|
* Common context for routes
|
|
12
13
|
*/
|
|
@@ -16,11 +17,13 @@ export type Context = {
|
|
|
16
17
|
service_context: RouterServiceContext;
|
|
17
18
|
|
|
18
19
|
token_payload?: JwtPayload;
|
|
19
|
-
|
|
20
|
+
token_error?: ServiceError;
|
|
20
21
|
/**
|
|
21
22
|
* Only on websocket endpoints.
|
|
22
23
|
*/
|
|
23
24
|
user_agent?: string;
|
|
25
|
+
|
|
26
|
+
logger: Logger;
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
export type BasicRouterRequest = {
|
|
@@ -29,7 +32,11 @@ export type BasicRouterRequest = {
|
|
|
29
32
|
hostname: string;
|
|
30
33
|
};
|
|
31
34
|
|
|
32
|
-
export type
|
|
35
|
+
export type ConextProviderOptions = {
|
|
36
|
+
logger: Logger;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type ContextProvider = (request: BasicRouterRequest, options: ConextProviderOptions) => Promise<Context>;
|
|
33
40
|
|
|
34
41
|
export type RequestEndpoint<
|
|
35
42
|
I,
|
package/src/runner/teardown.ts
CHANGED
|
@@ -14,7 +14,11 @@ export async function teardown(runnerConfig: utils.RunnerConfig) {
|
|
|
14
14
|
try {
|
|
15
15
|
logger.info(`Tearing down PowerSync instance...`);
|
|
16
16
|
const config = await utils.loadConfig(runnerConfig);
|
|
17
|
-
const serviceContext = new system.ServiceContextContainer(
|
|
17
|
+
const serviceContext = new system.ServiceContextContainer({
|
|
18
|
+
serviceMode: system.ServiceContextMode.TEARDOWN,
|
|
19
|
+
configuration: config
|
|
20
|
+
});
|
|
21
|
+
|
|
18
22
|
const moduleManager = container.getImplementation(modules.ModuleManager);
|
|
19
23
|
await moduleManager.initialize(serviceContext);
|
|
20
24
|
// This is mostly done to ensure that the storage is ready
|