@powersync/service-core 1.11.3 → 1.12.1
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 +56 -0
- package/dist/api/RouteAPI.d.ts +0 -4
- 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 +6 -6
- 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 +122 -74
- 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 +49 -34
- 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/dist/util/protocol-types.d.ts +9 -9
- package/dist/util/protocol-types.js.map +1 -1
- package/dist/util/utils.d.ts +1 -1
- package/package.json +6 -7
- package/src/api/RouteAPI.ts +0 -5
- 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 +10 -10
- 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 +162 -77
- package/src/sync/RequestTracker.ts +70 -3
- package/src/sync/sync.ts +72 -49
- 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/src/util/protocol-types.ts +15 -10
- 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
|
@@ -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({
|
|
@@ -202,13 +202,13 @@ async function debugSyncRules(apiHandler: RouteAPI, sync_rules: string) {
|
|
|
202
202
|
|
|
203
203
|
return {
|
|
204
204
|
valid: true,
|
|
205
|
-
bucket_definitions: rules.
|
|
206
|
-
let all_parameter_queries = [...d.
|
|
207
|
-
let all_data_queries = [...d.
|
|
205
|
+
bucket_definitions: rules.bucketDescriptors.map((d) => {
|
|
206
|
+
let all_parameter_queries = [...d.parameterQueries.values()].flat();
|
|
207
|
+
let all_data_queries = [...d.dataQueries.values()].flat();
|
|
208
208
|
return {
|
|
209
209
|
name: d.name,
|
|
210
|
-
bucket_parameters: d.
|
|
211
|
-
global_parameter_queries: d.
|
|
210
|
+
bucket_parameters: d.bucketParameters,
|
|
211
|
+
global_parameter_queries: d.globalParameterQueries.map((q) => {
|
|
212
212
|
return {
|
|
213
213
|
sql: q.sql
|
|
214
214
|
};
|
|
@@ -217,7 +217,7 @@ async function debugSyncRules(apiHandler: RouteAPI, sync_rules: string) {
|
|
|
217
217
|
return {
|
|
218
218
|
sql: q.sql,
|
|
219
219
|
table: q.sourceTable,
|
|
220
|
-
input_parameters: q.
|
|
220
|
+
input_parameters: q.inputParameters
|
|
221
221
|
};
|
|
222
222
|
}),
|
|
223
223
|
|
|
@@ -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
|