@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.
Files changed (114) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/api/RouteAPI.d.ts +0 -4
  3. package/dist/auth/CachedKeyCollector.js +2 -7
  4. package/dist/auth/CachedKeyCollector.js.map +1 -1
  5. package/dist/auth/CompoundKeyCollector.js.map +1 -1
  6. package/dist/auth/KeyCollector.d.ts +2 -2
  7. package/dist/auth/KeyStore.js +32 -14
  8. package/dist/auth/KeyStore.js.map +1 -1
  9. package/dist/auth/RemoteJWKSCollector.d.ts +1 -0
  10. package/dist/auth/RemoteJWKSCollector.js +39 -16
  11. package/dist/auth/RemoteJWKSCollector.js.map +1 -1
  12. package/dist/auth/auth-index.d.ts +1 -0
  13. package/dist/auth/auth-index.js +1 -0
  14. package/dist/auth/auth-index.js.map +1 -1
  15. package/dist/auth/utils.d.ts +6 -0
  16. package/dist/auth/utils.js +97 -0
  17. package/dist/auth/utils.js.map +1 -0
  18. package/dist/entry/commands/compact-action.js +4 -1
  19. package/dist/entry/commands/compact-action.js.map +1 -1
  20. package/dist/entry/commands/migrate-action.js +4 -1
  21. package/dist/entry/commands/migrate-action.js.map +1 -1
  22. package/dist/entry/commands/test-connection-action.js +4 -1
  23. package/dist/entry/commands/test-connection-action.js.map +1 -1
  24. package/dist/routes/RouterEngine.d.ts +2 -0
  25. package/dist/routes/RouterEngine.js +15 -10
  26. package/dist/routes/RouterEngine.js.map +1 -1
  27. package/dist/routes/auth.d.ts +5 -16
  28. package/dist/routes/auth.js +6 -4
  29. package/dist/routes/auth.js.map +1 -1
  30. package/dist/routes/configure-fastify.d.ts +3 -21
  31. package/dist/routes/configure-fastify.js +3 -6
  32. package/dist/routes/configure-fastify.js.map +1 -1
  33. package/dist/routes/configure-rsocket.js +28 -14
  34. package/dist/routes/configure-rsocket.js.map +1 -1
  35. package/dist/routes/endpoints/admin.js.map +1 -1
  36. package/dist/routes/endpoints/checkpointing.d.ts +4 -28
  37. package/dist/routes/endpoints/checkpointing.js.map +1 -1
  38. package/dist/routes/endpoints/route-endpoints-index.d.ts +1 -0
  39. package/dist/routes/endpoints/route-endpoints-index.js +1 -0
  40. package/dist/routes/endpoints/route-endpoints-index.js.map +1 -1
  41. package/dist/routes/endpoints/socket-route.js +22 -8
  42. package/dist/routes/endpoints/socket-route.js.map +1 -1
  43. package/dist/routes/endpoints/sync-rules.js +6 -6
  44. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  45. package/dist/routes/endpoints/sync-stream.d.ts +2 -14
  46. package/dist/routes/endpoints/sync-stream.js +28 -9
  47. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  48. package/dist/routes/route-register.js +10 -6
  49. package/dist/routes/route-register.js.map +1 -1
  50. package/dist/routes/router.d.ts +8 -7
  51. package/dist/routes/router.js.map +1 -1
  52. package/dist/runner/teardown.js +4 -1
  53. package/dist/runner/teardown.js.map +1 -1
  54. package/dist/sync/BucketChecksumState.d.ts +40 -18
  55. package/dist/sync/BucketChecksumState.js +122 -74
  56. package/dist/sync/BucketChecksumState.js.map +1 -1
  57. package/dist/sync/RequestTracker.d.ts +22 -1
  58. package/dist/sync/RequestTracker.js +51 -2
  59. package/dist/sync/RequestTracker.js.map +1 -1
  60. package/dist/sync/sync.d.ts +3 -5
  61. package/dist/sync/sync.js +49 -34
  62. package/dist/sync/sync.js.map +1 -1
  63. package/dist/system/ServiceContext.d.ts +19 -4
  64. package/dist/system/ServiceContext.js +20 -8
  65. package/dist/system/ServiceContext.js.map +1 -1
  66. package/dist/util/config/collectors/config-collector.js +4 -33
  67. package/dist/util/config/collectors/config-collector.js.map +1 -1
  68. package/dist/util/config/collectors/impl/yaml-env.d.ts +7 -0
  69. package/dist/util/config/collectors/impl/yaml-env.js +59 -0
  70. package/dist/util/config/collectors/impl/yaml-env.js.map +1 -0
  71. package/dist/util/config/compound-config-collector.js +18 -1
  72. package/dist/util/config/compound-config-collector.js.map +1 -1
  73. package/dist/util/config/types.d.ts +11 -0
  74. package/dist/util/protocol-types.d.ts +9 -9
  75. package/dist/util/protocol-types.js.map +1 -1
  76. package/dist/util/utils.d.ts +1 -1
  77. package/package.json +6 -7
  78. package/src/api/RouteAPI.ts +0 -5
  79. package/src/auth/CachedKeyCollector.ts +4 -6
  80. package/src/auth/CompoundKeyCollector.ts +2 -1
  81. package/src/auth/KeyCollector.ts +2 -2
  82. package/src/auth/KeyStore.ts +45 -20
  83. package/src/auth/RemoteJWKSCollector.ts +39 -16
  84. package/src/auth/auth-index.ts +1 -0
  85. package/src/auth/utils.ts +102 -0
  86. package/src/entry/commands/compact-action.ts +4 -1
  87. package/src/entry/commands/migrate-action.ts +4 -1
  88. package/src/entry/commands/test-connection-action.ts +4 -1
  89. package/src/routes/RouterEngine.ts +21 -11
  90. package/src/routes/auth.ts +7 -6
  91. package/src/routes/configure-fastify.ts +6 -8
  92. package/src/routes/configure-rsocket.ts +33 -18
  93. package/src/routes/endpoints/admin.ts +5 -5
  94. package/src/routes/endpoints/checkpointing.ts +2 -2
  95. package/src/routes/endpoints/route-endpoints-index.ts +1 -0
  96. package/src/routes/endpoints/socket-route.ts +27 -11
  97. package/src/routes/endpoints/sync-rules.ts +10 -10
  98. package/src/routes/endpoints/sync-stream.ts +34 -11
  99. package/src/routes/route-register.ts +10 -7
  100. package/src/routes/router.ts +11 -4
  101. package/src/runner/teardown.ts +5 -1
  102. package/src/sync/BucketChecksumState.ts +162 -77
  103. package/src/sync/RequestTracker.ts +70 -3
  104. package/src/sync/sync.ts +72 -49
  105. package/src/system/ServiceContext.ts +31 -12
  106. package/src/util/config/collectors/config-collector.ts +4 -40
  107. package/src/util/config/collectors/impl/yaml-env.ts +67 -0
  108. package/src/util/config/compound-config-collector.ts +22 -5
  109. package/src/util/config/types.ts +13 -0
  110. package/src/util/protocol-types.ts +15 -10
  111. package/test/src/auth.test.ts +29 -11
  112. package/test/src/config.test.ts +72 -0
  113. package/test/src/sync/BucketChecksumState.test.ts +32 -18
  114. 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: Buffer) => {
26
- const { token, user_agent } = RSocketContextMeta.decode(deserialize(data) as any);
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, errors: token_errors } = await generateContext(options.service_context, extracted_token);
40
+ const { context, tokenError } = await generateContext(options.service_context, extracted_token);
36
41
  if (context?.token_payload == null) {
37
- throw new errors.AuthorizationError(token_errors ?? 'Authentication required');
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
- token_errors: token_errors,
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
- throw new errors.AuthorizationError('No token provided');
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
- logger.error(ex);
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: Buffer) => {
61
- return RSocketRequestMeta.decode(deserialize(meta) as any);
63
+ metaDecoder: async (meta: TypedBuffer) => {
64
+ return RSocketRequestMeta.decode(decodeTyped(meta) as any);
62
65
  },
63
- payloadDecoder: async (rawData?: Buffer) => rawData && deserialize(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!.getAPI();
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!.getAPI();
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!.getAPI();
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!.getAPI();
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!.getAPI();
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!.getAPI();
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!.getAPI();
57
+ const apiHandler = service_context.routerEngine.getAPI();
58
58
 
59
59
  const { replicationHead, writeCheckpoint } = await util.createWriteCheckpoint({
60
60
  userId: user_id,
@@ -1,5 +1,6 @@
1
1
  export * from './admin.js';
2
2
  export * from './checkpointing.js';
3
+ export * from './probes.js';
3
4
  export * from './socket-route.js';
4
5
  export * from './sync-rules.js';
5
6
  export * from './sync-stream.js';
@@ -1,4 +1,4 @@
1
- import { ErrorCode, errors, logger, schema } from '@powersync/lib-services-framework';
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!.closed) {
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!.getAPI().getParseSyncRulesOptions());
81
+ const syncRules = bucketStorage.getParsedSyncRules(routerEngine.getAPI().getParseSyncRulesOptions());
68
82
 
69
- const removeStopHandler = routerEngine!.addStopHandler(() => {
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
- user_id: syncParams.user_id,
146
- client_id: params.client_id,
147
- user_agent: context.user_agent,
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!.getAPI();
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!.getAPI();
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!.getAPI();
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!.getAPI();
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.bucket_descriptors.map((d) => {
206
- let all_parameter_queries = [...d.parameter_queries.values()].flat();
207
- let all_data_queries = [...d.data_queries.values()].flat();
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.bucket_parameters,
211
- global_parameter_queries: d.global_parameter_queries.map((q) => {
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.input_parameters
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
- if (routerEngine!.closed) {
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!.getAPI().getParseSyncRulesOptions());
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
- const deregister = routerEngine!.addStopHandler(() => {
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
- user_id: syncParams.user_id,
103
- client_id: clientId,
104
- user_agent: userAgent,
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
- logger.error(`Request failed`, serviceError);
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
- logger.info(`${e.method} ${request.url}`, {
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
  }
@@ -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 & { routerEngine: RouterEngine };
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
- token_errors?: string[];
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 ContextProvider = (request: BasicRouterRequest) => Promise<Context>;
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,
@@ -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(config);
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