@trpc/server 10.28.2 → 10.29.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 (56) hide show
  1. package/dist/adapters/aws-lambda/index.js +1 -1
  2. package/dist/adapters/aws-lambda/index.mjs +1 -1
  3. package/dist/adapters/express.js +5 -4
  4. package/dist/adapters/express.mjs +5 -4
  5. package/dist/adapters/fastify/fastifyRequestHandler.d.ts +1 -1
  6. package/dist/adapters/fastify/fastifyRequestHandler.d.ts.map +1 -1
  7. package/dist/adapters/fastify/index.js +51 -15
  8. package/dist/adapters/fastify/index.mjs +51 -15
  9. package/dist/adapters/fetch/fetchRequestHandler.d.ts.map +1 -1
  10. package/dist/adapters/fetch/index.js +67 -20
  11. package/dist/adapters/fetch/index.mjs +67 -20
  12. package/dist/adapters/next.js +3 -2
  13. package/dist/adapters/next.mjs +3 -2
  14. package/dist/adapters/node-http/content-type/form-data/index.js +19 -46
  15. package/dist/adapters/node-http/content-type/form-data/index.mjs +19 -40
  16. package/dist/adapters/node-http/content-type/form-data/streamSlice.d.ts +7 -3
  17. package/dist/adapters/node-http/content-type/form-data/streamSlice.d.ts.map +1 -1
  18. package/dist/adapters/node-http/index.js +5 -4
  19. package/dist/adapters/node-http/index.mjs +5 -4
  20. package/dist/adapters/node-http/nodeHTTPRequestHandler.d.ts.map +1 -1
  21. package/dist/adapters/node-http/types.d.ts +11 -1
  22. package/dist/adapters/node-http/types.d.ts.map +1 -1
  23. package/dist/adapters/standalone.js +5 -4
  24. package/dist/adapters/standalone.mjs +5 -4
  25. package/dist/batchStreamFormatter-2c1405a1.js +31 -0
  26. package/dist/batchStreamFormatter-93cdcdd4.js +32 -0
  27. package/dist/batchStreamFormatter-fc1ffb26.mjs +30 -0
  28. package/dist/http/batchStreamFormatter.d.ts +24 -0
  29. package/dist/http/batchStreamFormatter.d.ts.map +1 -0
  30. package/dist/http/index.d.ts +1 -0
  31. package/dist/http/index.d.ts.map +1 -1
  32. package/dist/http/index.js +3 -1
  33. package/dist/http/index.mjs +2 -1
  34. package/dist/http/internals/types.d.ts +7 -0
  35. package/dist/http/internals/types.d.ts.map +1 -1
  36. package/dist/http/resolveHTTPResponse.d.ts +37 -2
  37. package/dist/http/resolveHTTPResponse.d.ts.map +1 -1
  38. package/dist/{nodeHTTPRequestHandler-5bbf93f1.js → nodeHTTPRequestHandler-071d36b5.js} +44 -13
  39. package/dist/{nodeHTTPRequestHandler-bd47641d.js → nodeHTTPRequestHandler-51d58a23.js} +47 -12
  40. package/dist/{nodeHTTPRequestHandler-0f979796.mjs → nodeHTTPRequestHandler-bb12529f.mjs} +44 -13
  41. package/dist/resolveHTTPResponse-1f03fdfd.js +284 -0
  42. package/dist/resolveHTTPResponse-dd3677b3.mjs +282 -0
  43. package/dist/resolveHTTPResponse-e0047ea2.js +256 -0
  44. package/package.json +4 -4
  45. package/src/adapters/fastify/fastifyRequestHandler.ts +65 -17
  46. package/src/adapters/fetch/fetchRequestHandler.ts +73 -25
  47. package/src/adapters/node-http/content-type/form-data/streamSlice.ts +19 -21
  48. package/src/adapters/node-http/nodeHTTPRequestHandler.ts +53 -12
  49. package/src/adapters/node-http/types.ts +11 -1
  50. package/src/http/batchStreamFormatter.ts +29 -0
  51. package/src/http/index.ts +1 -0
  52. package/src/http/internals/types.ts +8 -0
  53. package/src/http/resolveHTTPResponse.ts +339 -124
  54. package/dist/resolveHTTPResponse-4e576698.mjs +0 -174
  55. package/dist/resolveHTTPResponse-9523b4e3.js +0 -176
  56. package/dist/resolveHTTPResponse-ba557d3e.js +0 -166
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Format a batch response as a line-delimited JSON stream
3
+ * that the `unstable_httpBatchStreamLink` can parse:
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const formatter = getBatchStreamFormatter();
8
+ * res.send(formatter(1, 'response #2'));
9
+ * res.send(formatter(0, 'response #1'));
10
+ * res.send(formatter.end());
11
+ * ```
12
+ *
13
+ * Expected format:
14
+ * ```json
15
+ * {"1":"response #2"
16
+ * ,"0":"response #1"
17
+ * }
18
+ * ```
19
+ */
20
+ export function getBatchStreamFormatter() {
21
+ let first = true;
22
+ function format(index: number, string: string) {
23
+ const prefix = first ? '{' : ',';
24
+ first = false;
25
+ return `${prefix}"${index}":${string}\n`;
26
+ }
27
+ format.end = () => '}';
28
+ return format;
29
+ }
package/src/http/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './getHTTPStatusCode';
2
2
  export * from './resolveHTTPResponse';
3
3
  export * from './types';
4
+ export * from './batchStreamFormatter';
@@ -17,6 +17,8 @@ export interface HTTPResponse {
17
17
  body?: string;
18
18
  }
19
19
 
20
+ export type ResponseChunk = [procedureIndex: number, responseBody: string];
21
+
20
22
  /**
21
23
  * @internal
22
24
  */
@@ -29,4 +31,10 @@ export type ResponseMetaFn<TRouter extends AnyRouter> = (opts: {
29
31
  paths?: string[];
30
32
  type: ProcedureType | 'unknown';
31
33
  errors: TRPCError[];
34
+ /**
35
+ * `true` if the `ResponseMeta` are being
36
+ * generated without knowing the response data
37
+ * (e.g. for streaming requests).
38
+ */
39
+ eagerGeneration?: boolean;
32
40
  }) => ResponseMeta;
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
1
  import {
3
2
  AnyRouter,
4
3
  callProcedure,
@@ -16,7 +15,7 @@ import {
16
15
  getJsonContentTypeInputs,
17
16
  } from './contentType';
18
17
  import { getHTTPStatusCode } from './getHTTPStatusCode';
19
- import { HTTPHeaders, HTTPResponse } from './internals/types';
18
+ import { HTTPHeaders, HTTPResponse, ResponseChunk } from './internals/types';
20
19
  import { HTTPBaseHandlerOptions, HTTPRequest } from './types';
21
20
 
22
21
  const HTTP_METHOD_PROCEDURE_TYPE_MAP: Record<
@@ -31,6 +30,12 @@ const fallbackContentTypeHandler = {
31
30
  getInputs: getJsonContentTypeInputs,
32
31
  };
33
32
 
33
+ type PartialBy<TBaseType, TKey extends keyof TBaseType> = Omit<
34
+ TBaseType,
35
+ TKey
36
+ > &
37
+ Partial<Pick<TBaseType, TKey>>;
38
+
34
39
  interface ResolveHTTPRequestOptions<
35
40
  TRouter extends AnyRouter,
36
41
  TRequest extends HTTPRequest,
@@ -41,72 +46,245 @@ interface ResolveHTTPRequestOptions<
41
46
  error?: Maybe<TRPCError>;
42
47
  contentTypeHandler?: BaseContentTypeHandler<any>;
43
48
  preprocessedBody?: boolean;
49
+ /**
50
+ * Called as soon as the response head is known.
51
+ * When streaming, headers will have been generated
52
+ * **without** knowing the response body.
53
+ *
54
+ * Without this callback, streaming is disabled.
55
+ */
56
+ unstable_onHead: (
57
+ headResponse: Omit<HTTPResponse, 'body'>,
58
+ isStreaming: boolean,
59
+ ) => void;
60
+ /**
61
+ * Called for every procedure with `[index, result]`.
62
+ *
63
+ * Will be called a single time with `index = -1` if
64
+ * - response is an error
65
+ * - response is empty (HEAD request)
66
+ *
67
+ * Without this callback, streaming is disabled.
68
+ */
69
+ unstable_onChunk: (chunk: ResponseChunk) => void;
44
70
  }
45
71
 
46
- export async function resolveHTTPResponse<
72
+ function initResponse<
47
73
  TRouter extends AnyRouter,
48
74
  TRequest extends HTTPRequest,
49
- >(opts: ResolveHTTPRequestOptions<TRouter, TRequest>): Promise<HTTPResponse> {
50
- const { router, req } = opts;
51
- const contentTypeHandler =
52
- opts.contentTypeHandler ?? fallbackContentTypeHandler;
75
+ >(initOpts: {
76
+ ctx: inferRouterContext<TRouter> | undefined;
77
+ paths: string[] | undefined;
78
+ type: ProcedureType | 'unknown';
79
+ responseMeta?: HTTPBaseHandlerOptions<TRouter, TRequest>['responseMeta'];
80
+ untransformedJSON?:
81
+ | TRPCResponse<unknown, inferRouterError<TRouter>>
82
+ | TRPCResponse<unknown, inferRouterError<TRouter>>[]
83
+ | undefined;
84
+ errors?: TRPCError[];
85
+ }): HTTPResponse {
86
+ const {
87
+ ctx,
88
+ paths,
89
+ type,
90
+ responseMeta,
91
+ untransformedJSON,
92
+ errors = [],
93
+ } = initOpts;
53
94
 
54
- const batchingEnabled = opts.batching?.enabled ?? true;
55
- if (req.method === 'HEAD') {
56
- // can be used for lambda warmup
57
- return {
58
- status: 204,
59
- };
95
+ let status = untransformedJSON ? getHTTPStatusCode(untransformedJSON) : 200;
96
+ const headers: HTTPHeaders = {
97
+ 'Content-Type': 'application/json',
98
+ };
99
+
100
+ const eagerGeneration = !untransformedJSON;
101
+ const data = eagerGeneration
102
+ ? []
103
+ : Array.isArray(untransformedJSON)
104
+ ? untransformedJSON
105
+ : [untransformedJSON];
106
+
107
+ const meta =
108
+ responseMeta?.({
109
+ ctx,
110
+ paths,
111
+ type,
112
+ data,
113
+ errors,
114
+ eagerGeneration,
115
+ }) ?? {};
116
+
117
+ for (const [key, value] of Object.entries(meta.headers ?? {})) {
118
+ headers[key] = value;
119
+ }
120
+ if (meta.status) {
121
+ status = meta.status;
60
122
  }
61
- const type =
62
- HTTP_METHOD_PROCEDURE_TYPE_MAP[req.method] ?? ('unknown' as const);
63
- let ctx: inferRouterContext<TRouter> | undefined = undefined;
64
- let paths: string[] | undefined = undefined;
65
123
 
66
- const isBatchCall = !!req.query.get('batch');
67
- type TRouterError = inferRouterError<TRouter>;
68
- type TRouterResponse = TRPCResponse<unknown, TRouterError>;
69
-
70
- function endResponse(
71
- untransformedJSON: TRouterResponse | TRouterResponse[],
72
- errors: TRPCError[],
73
- ): HTTPResponse {
74
- let status = getHTTPStatusCode(untransformedJSON);
75
- const headers: HTTPHeaders = {
76
- 'Content-Type': 'application/json',
124
+ return {
125
+ status,
126
+ headers,
127
+ };
128
+ }
129
+
130
+ async function inputToProcedureCall<
131
+ TRouter extends AnyRouter,
132
+ TRequest extends HTTPRequest,
133
+ >(procedureOpts: {
134
+ opts: Pick<
135
+ ResolveHTTPRequestOptions<TRouter, TRequest>,
136
+ 'router' | 'onError' | 'req'
137
+ >;
138
+ ctx: inferRouterContext<TRouter> | undefined;
139
+ type: 'query' | 'mutation';
140
+ input: unknown;
141
+ path: string;
142
+ }): Promise<TRPCResponse<unknown, inferRouterError<TRouter>>> {
143
+ const { opts, ctx, type, input, path } = procedureOpts;
144
+ try {
145
+ const data = await callProcedure({
146
+ procedures: opts.router._def.procedures,
147
+ path,
148
+ rawInput: input,
149
+ ctx,
150
+ type,
151
+ });
152
+ return {
153
+ result: {
154
+ data,
155
+ },
77
156
  };
157
+ } catch (cause) {
158
+ const error = getTRPCErrorFromUnknown(cause);
78
159
 
79
- const meta =
80
- opts.responseMeta?.({
81
- ctx,
82
- paths,
160
+ opts.onError?.({ error, path, input, ctx, type: type, req: opts.req });
161
+
162
+ return {
163
+ error: getErrorShape({
164
+ config: opts.router._def._config,
165
+ error,
83
166
  type,
84
- data: Array.isArray(untransformedJSON)
85
- ? untransformedJSON
86
- : [untransformedJSON],
87
- errors,
88
- }) ?? {};
167
+ path,
168
+ input,
169
+ ctx,
170
+ }),
171
+ };
172
+ }
173
+ }
89
174
 
90
- for (const [key, value] of Object.entries(meta.headers ?? {})) {
91
- headers[key] = value;
92
- }
93
- if (meta.status) {
94
- status = meta.status;
95
- }
175
+ function caughtErrorToData<
176
+ TRouter extends AnyRouter,
177
+ TRequest extends HTTPRequest,
178
+ >(
179
+ cause: unknown,
180
+ errorOpts: {
181
+ opts: Pick<
182
+ ResolveHTTPRequestOptions<TRouter, TRequest>,
183
+ 'router' | 'onError' | 'req'
184
+ >;
185
+ ctx: inferRouterContext<TRouter> | undefined;
186
+ type: ProcedureType | 'unknown';
187
+ path?: string;
188
+ input?: unknown;
189
+ },
190
+ ) {
191
+ const { router, req, onError } = errorOpts.opts;
192
+ const error = getTRPCErrorFromUnknown(cause);
193
+ onError?.({
194
+ error,
195
+ path: errorOpts.path,
196
+ input: errorOpts.input,
197
+ ctx: errorOpts.ctx,
198
+ type: errorOpts.type,
199
+ req,
200
+ });
201
+ const untransformedJSON = {
202
+ error: getErrorShape({
203
+ config: router._def._config,
204
+ error,
205
+ type: errorOpts.type,
206
+ path: errorOpts.path,
207
+ input: errorOpts.input,
208
+ ctx: errorOpts.ctx,
209
+ }),
210
+ };
211
+ const transformedJSON = transformTRPCResponse(
212
+ router._def._config,
213
+ untransformedJSON,
214
+ );
215
+ const body = JSON.stringify(transformedJSON);
216
+ return {
217
+ error,
218
+ untransformedJSON,
219
+ body,
220
+ };
221
+ }
96
222
 
97
- const transformedJSON = transformTRPCResponse(
98
- router._def._config,
99
- untransformedJSON,
100
- );
223
+ /**
224
+ * Since `resolveHTTPResponse` is a public API (community adapters),
225
+ * let's give it a strong type signature to increase discoverability.
226
+ */
101
227
 
102
- const body = JSON.stringify(transformedJSON);
228
+ /**
229
+ * Non-streaming signature for `resolveHTTPResponse`:
230
+ * @param opts.unstable_onHead `undefined`
231
+ * @param opts.unstable_onChunk `undefined`
232
+ * @returns `Promise<HTTPResponse>`
233
+ */
234
+ export async function resolveHTTPResponse<
235
+ TRouter extends AnyRouter,
236
+ TRequest extends HTTPRequest,
237
+ >(
238
+ opts: Omit<
239
+ ResolveHTTPRequestOptions<TRouter, TRequest>,
240
+ 'unstable_onHead' | 'unstable_onChunk'
241
+ >,
242
+ ): Promise<HTTPResponse>;
243
+ /**
244
+ * Streaming signature for `resolveHTTPResponse`:
245
+ * @param opts.unstable_onHead called as soon as the response head is known
246
+ * @param opts.unstable_onChunk called for every procedure with `[index, result]`
247
+ * @returns `Promise<void>` since the response is streamed
248
+ */
249
+ export async function resolveHTTPResponse<
250
+ TRouter extends AnyRouter,
251
+ TRequest extends HTTPRequest,
252
+ >(opts: ResolveHTTPRequestOptions<TRouter, TRequest>): Promise<void>;
253
+ // implementation
254
+ export async function resolveHTTPResponse<
255
+ TRouter extends AnyRouter,
256
+ TRequest extends HTTPRequest,
257
+ >(
258
+ opts: PartialBy<
259
+ ResolveHTTPRequestOptions<TRouter, TRequest>,
260
+ 'unstable_onHead' | 'unstable_onChunk'
261
+ >,
262
+ ): Promise<HTTPResponse | void> {
263
+ const { router, req, unstable_onHead, unstable_onChunk } = opts;
103
264
 
104
- return {
105
- body,
106
- status,
107
- headers,
265
+ if (req.method === 'HEAD') {
266
+ // can be used for lambda warmup
267
+ const headResponse: HTTPResponse = {
268
+ status: 204,
108
269
  };
270
+ unstable_onHead?.(headResponse, false);
271
+ unstable_onChunk?.([-1, '']);
272
+ return headResponse;
109
273
  }
274
+ const contentTypeHandler =
275
+ opts.contentTypeHandler ?? fallbackContentTypeHandler;
276
+ const batchingEnabled = opts.batching?.enabled ?? true;
277
+ const type =
278
+ HTTP_METHOD_PROCEDURE_TYPE_MAP[req.method] ?? ('unknown' as const);
279
+ let ctx: inferRouterContext<TRouter> | undefined = undefined;
280
+ let paths: string[] | undefined;
281
+
282
+ const isBatchCall = !!req.query.get('batch');
283
+ const isStreamCall =
284
+ isBatchCall &&
285
+ unstable_onHead &&
286
+ unstable_onChunk &&
287
+ req.headers['trpc-batch-mode'] === 'stream';
110
288
 
111
289
  try {
112
290
  if (opts.error) {
@@ -138,91 +316,128 @@ export async function resolveHTTPResponse<
138
316
 
139
317
  paths = isBatchCall ? opts.path.split(',') : [opts.path];
140
318
  ctx = await opts.createContext();
319
+ const promises = paths.map((path, index) =>
320
+ inputToProcedureCall({ opts, ctx, type, input: inputs[index], path }),
321
+ );
141
322
 
142
- const rawResults = await Promise.all(
143
- paths.map(async (path, index) => {
144
- const input = inputs[index];
323
+ if (!isStreamCall) {
324
+ /**
325
+ * Non-streaming response:
326
+ * - await all responses in parallel, blocking on the slowest one
327
+ * - create headers with known response body
328
+ * - return a complete HTTPResponse
329
+ */
145
330
 
146
- try {
147
- const output = await callProcedure({
148
- procedures: router._def.procedures,
149
- path,
150
- rawInput: input,
151
- ctx,
152
- type,
153
- });
154
- return {
155
- input,
156
- path,
157
- data: output,
158
- };
159
- } catch (cause) {
160
- const error = getTRPCErrorFromUnknown(cause);
161
-
162
- opts.onError?.({ error, path, input, ctx, type: type, req });
163
- return {
164
- input,
165
- path,
166
- error,
167
- };
168
- }
169
- }),
170
- );
171
- const errors = rawResults.flatMap((obj) => (obj.error ? [obj.error] : []));
172
- const resultEnvelopes = rawResults.map((obj): TRouterResponse => {
173
- const { path, input } = obj;
174
-
175
- if (obj.error) {
176
- return {
177
- error: getErrorShape({
178
- config: router._def._config,
179
- error: obj.error,
180
- type,
181
- path,
182
- input,
183
- ctx,
184
- }),
185
- };
186
- } else {
187
- return {
188
- result: {
189
- data: obj.data,
190
- },
191
- };
192
- }
331
+ const untransformedJSON = await Promise.all(promises);
332
+ const errors = untransformedJSON.flatMap((response) =>
333
+ 'error' in response ? [response.error] : [],
334
+ );
335
+
336
+ const headResponse = initResponse({
337
+ ctx,
338
+ paths,
339
+ type,
340
+ responseMeta: opts.responseMeta,
341
+ untransformedJSON,
342
+ errors,
343
+ });
344
+ unstable_onHead?.(headResponse, false);
345
+
346
+ // return body stuff
347
+ const result = isBatchCall ? untransformedJSON : untransformedJSON[0]!; // eslint-disable-line @typescript-eslint/no-non-null-assertion -- `untransformedJSON` should be the length of `paths` which should be at least 1 otherwise there wouldn't be a request at all
348
+ const transformedJSON = transformTRPCResponse(
349
+ router._def._config,
350
+ result,
351
+ );
352
+ const body = JSON.stringify(transformedJSON);
353
+ unstable_onChunk?.([-1, body]);
354
+
355
+ return {
356
+ status: headResponse.status,
357
+ headers: headResponse.headers,
358
+ body,
359
+ };
360
+ }
361
+
362
+ /**
363
+ * Streaming response:
364
+ * - block on none, call `onChunk` as soon as each response is ready
365
+ * - create headers with minimal data (cannot know the response body in advance)
366
+ * - return void
367
+ */
368
+ const headResponse = initResponse({
369
+ ctx,
370
+ paths,
371
+ type,
372
+ responseMeta: opts.responseMeta,
193
373
  });
374
+ unstable_onHead(headResponse, true);
375
+
376
+ const indexedPromises = new Map(
377
+ promises.map((promise, index) => [
378
+ index,
379
+ promise.then((r) => [index, r] as const),
380
+ ]),
381
+ );
382
+ for (let i = 0; i < paths.length; i++) {
383
+ const [index, untransformedJSON] = await Promise.race(
384
+ indexedPromises.values(),
385
+ );
386
+ indexedPromises.delete(index);
387
+
388
+ try {
389
+ const transformedJSON = transformTRPCResponse(
390
+ router._def._config,
391
+ untransformedJSON,
392
+ );
393
+ const body = JSON.stringify(transformedJSON);
394
+
395
+ unstable_onChunk([index, body]);
396
+ } catch (cause) {
397
+ const path = paths[index];
398
+ const input = inputs[index];
399
+ const { body } = caughtErrorToData(cause, {
400
+ opts,
401
+ ctx,
402
+ type,
403
+ path,
404
+ input,
405
+ });
194
406
 
195
- const result = isBatchCall ? resultEnvelopes : resultEnvelopes[0]!;
196
- return endResponse(result, errors);
407
+ unstable_onChunk([index, body]);
408
+ }
409
+ }
410
+ return;
197
411
  } catch (cause) {
198
412
  // we get here if
199
413
  // - batching is called when it's not enabled
200
414
  // - `createContext()` throws
415
+ // - `router._def._config.transformer.output.serialize()` throws
201
416
  // - post body is too large
202
417
  // - input deserialization fails
203
418
  // - `errorFormatter` return value is malformed
204
- const error = getTRPCErrorFromUnknown(cause);
419
+ const { error, untransformedJSON, body } = caughtErrorToData(cause, {
420
+ opts,
421
+ ctx,
422
+ type,
423
+ });
205
424
 
206
- opts.onError?.({
207
- error,
208
- path: undefined,
209
- input: undefined,
425
+ const headResponse = initResponse({
210
426
  ctx,
211
- type: type,
212
- req,
427
+ paths,
428
+ type,
429
+ responseMeta: opts.responseMeta,
430
+ untransformedJSON,
431
+ errors: [error],
213
432
  });
214
- return endResponse(
215
- {
216
- error: getErrorShape({
217
- config: router._def._config,
218
- error,
219
- type,
220
- path: undefined,
221
- input: undefined,
222
- ctx,
223
- }),
224
- },
225
- [error],
226
- );
433
+ unstable_onHead?.(headResponse, false);
434
+
435
+ unstable_onChunk?.([-1, body]);
436
+
437
+ return {
438
+ status: headResponse.status,
439
+ headers: headResponse.headers,
440
+ body,
441
+ };
227
442
  }
228
443
  }