@trpc/server 10.28.2 → 10.29.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.
Files changed (53) 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 +50 -15
  8. package/dist/adapters/fastify/index.mjs +50 -15
  9. package/dist/adapters/fetch/fetchRequestHandler.d.ts.map +1 -1
  10. package/dist/adapters/fetch/index.js +66 -20
  11. package/dist/adapters/fetch/index.mjs +66 -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/standalone.js +5 -4
  22. package/dist/adapters/standalone.mjs +5 -4
  23. package/dist/batchStreamFormatter-2c1405a1.js +31 -0
  24. package/dist/batchStreamFormatter-93cdcdd4.js +32 -0
  25. package/dist/batchStreamFormatter-fc1ffb26.mjs +30 -0
  26. package/dist/http/batchStreamFormatter.d.ts +24 -0
  27. package/dist/http/batchStreamFormatter.d.ts.map +1 -0
  28. package/dist/http/index.d.ts +1 -0
  29. package/dist/http/index.d.ts.map +1 -1
  30. package/dist/http/index.js +3 -1
  31. package/dist/http/index.mjs +2 -1
  32. package/dist/http/internals/types.d.ts +7 -0
  33. package/dist/http/internals/types.d.ts.map +1 -1
  34. package/dist/http/resolveHTTPResponse.d.ts +36 -2
  35. package/dist/http/resolveHTTPResponse.d.ts.map +1 -1
  36. package/dist/{nodeHTTPRequestHandler-bd47641d.js → nodeHTTPRequestHandler-53b9fc2d.js} +44 -12
  37. package/dist/{nodeHTTPRequestHandler-0f979796.mjs → nodeHTTPRequestHandler-bdad2781.mjs} +42 -13
  38. package/dist/{nodeHTTPRequestHandler-5bbf93f1.js → nodeHTTPRequestHandler-e637755a.js} +42 -13
  39. package/dist/resolveHTTPResponse-2bdf2cd7.js +256 -0
  40. package/dist/resolveHTTPResponse-2c307e04.js +284 -0
  41. package/dist/resolveHTTPResponse-a9be3d96.mjs +282 -0
  42. package/package.json +4 -4
  43. package/src/adapters/fastify/fastifyRequestHandler.ts +64 -17
  44. package/src/adapters/fetch/fetchRequestHandler.ts +72 -25
  45. package/src/adapters/node-http/content-type/form-data/streamSlice.ts +19 -21
  46. package/src/adapters/node-http/nodeHTTPRequestHandler.ts +51 -12
  47. package/src/http/batchStreamFormatter.ts +29 -0
  48. package/src/http/index.ts +1 -0
  49. package/src/http/internals/types.ts +8 -0
  50. package/src/http/resolveHTTPResponse.ts +335 -124
  51. package/dist/resolveHTTPResponse-4e576698.mjs +0 -174
  52. package/dist/resolveHTTPResponse-9523b4e3.js +0 -176
  53. package/dist/resolveHTTPResponse-ba557d3e.js +0 -166
@@ -0,0 +1,282 @@
1
+ import { b as callProcedure } from './config-65c5b6d1.mjs';
2
+ import { T as TRPCError, a as getTRPCErrorFromUnknown } from './TRPCError-2b10c8d2.mjs';
3
+ import { t as transformTRPCResponse, g as getErrorShape } from './transformTRPCResponse-3dca0b20.mjs';
4
+ import { g as getJsonContentTypeInputs } from './contentType-acc3be52.mjs';
5
+ import { b as getHTTPStatusCode } from './index-044a193b.mjs';
6
+
7
+ const HTTP_METHOD_PROCEDURE_TYPE_MAP = {
8
+ GET: 'query',
9
+ POST: 'mutation'
10
+ };
11
+ const fallbackContentTypeHandler = {
12
+ getInputs: getJsonContentTypeInputs
13
+ };
14
+ function initResponse(initOpts) {
15
+ const { ctx , paths , type , responseMeta , untransformedJSON , errors =[] , } = initOpts;
16
+ let status = untransformedJSON ? getHTTPStatusCode(untransformedJSON) : 200;
17
+ const headers = {
18
+ 'Content-Type': 'application/json'
19
+ };
20
+ const eagerGeneration = !untransformedJSON;
21
+ const data = eagerGeneration ? [] : Array.isArray(untransformedJSON) ? untransformedJSON : [
22
+ untransformedJSON
23
+ ];
24
+ const meta = responseMeta?.({
25
+ ctx,
26
+ paths,
27
+ type,
28
+ data,
29
+ errors,
30
+ eagerGeneration
31
+ }) ?? {};
32
+ for (const [key, value] of Object.entries(meta.headers ?? {})){
33
+ headers[key] = value;
34
+ }
35
+ if (meta.status) {
36
+ status = meta.status;
37
+ }
38
+ return {
39
+ status,
40
+ headers
41
+ };
42
+ }
43
+ async function inputToProcedureCall(procedureOpts) {
44
+ const { opts , ctx , type , input , path } = procedureOpts;
45
+ try {
46
+ const data = await callProcedure({
47
+ procedures: opts.router._def.procedures,
48
+ path,
49
+ rawInput: input,
50
+ ctx,
51
+ type
52
+ });
53
+ return {
54
+ result: {
55
+ data
56
+ }
57
+ };
58
+ } catch (cause) {
59
+ const error = getTRPCErrorFromUnknown(cause);
60
+ opts.onError?.({
61
+ error,
62
+ path,
63
+ input,
64
+ ctx,
65
+ type: type,
66
+ req: opts.req
67
+ });
68
+ return {
69
+ error: getErrorShape({
70
+ config: opts.router._def._config,
71
+ error,
72
+ type,
73
+ path,
74
+ input,
75
+ ctx
76
+ })
77
+ };
78
+ }
79
+ }
80
+ function caughtErrorToData(cause, errorOpts) {
81
+ const { router , req , onError } = errorOpts.opts;
82
+ const error = getTRPCErrorFromUnknown(cause);
83
+ onError?.({
84
+ error,
85
+ path: errorOpts.path,
86
+ input: errorOpts.input,
87
+ ctx: errorOpts.ctx,
88
+ type: errorOpts.type,
89
+ req
90
+ });
91
+ const untransformedJSON = {
92
+ error: getErrorShape({
93
+ config: router._def._config,
94
+ error,
95
+ type: errorOpts.type,
96
+ path: errorOpts.path,
97
+ input: errorOpts.input,
98
+ ctx: errorOpts.ctx
99
+ })
100
+ };
101
+ const transformedJSON = transformTRPCResponse(router._def._config, untransformedJSON);
102
+ const body = JSON.stringify(transformedJSON);
103
+ return {
104
+ error,
105
+ untransformedJSON,
106
+ body
107
+ };
108
+ }
109
+ // implementation
110
+ async function resolveHTTPResponse(opts) {
111
+ const { router , req , onHead , onChunk } = opts;
112
+ if (req.method === 'HEAD') {
113
+ // can be used for lambda warmup
114
+ const headResponse = {
115
+ status: 204
116
+ };
117
+ onHead?.(headResponse);
118
+ onChunk?.([
119
+ -1,
120
+ ''
121
+ ]);
122
+ return headResponse;
123
+ }
124
+ const contentTypeHandler = opts.contentTypeHandler ?? fallbackContentTypeHandler;
125
+ const batchingEnabled = opts.batching?.enabled ?? true;
126
+ const type = HTTP_METHOD_PROCEDURE_TYPE_MAP[req.method] ?? 'unknown';
127
+ let ctx = undefined;
128
+ let paths;
129
+ const isBatchCall = !!req.query.get('batch');
130
+ const isStreamCall = isBatchCall && onHead && onChunk && req.headers['trpc-batch-mode'] === 'stream';
131
+ try {
132
+ if (opts.error) {
133
+ throw opts.error;
134
+ }
135
+ if (isBatchCall && !batchingEnabled) {
136
+ throw new Error(`Batching is not enabled on the server`);
137
+ }
138
+ /* istanbul ignore if -- @preserve */ if (type === 'subscription') {
139
+ throw new TRPCError({
140
+ message: 'Subscriptions should use wsLink',
141
+ code: 'METHOD_NOT_SUPPORTED'
142
+ });
143
+ }
144
+ if (type === 'unknown') {
145
+ throw new TRPCError({
146
+ message: `Unexpected request method ${req.method}`,
147
+ code: 'METHOD_NOT_SUPPORTED'
148
+ });
149
+ }
150
+ const inputs = await contentTypeHandler.getInputs({
151
+ isBatchCall,
152
+ req,
153
+ router,
154
+ preprocessedBody: opts.preprocessedBody ?? false
155
+ });
156
+ paths = isBatchCall ? opts.path.split(',') : [
157
+ opts.path
158
+ ];
159
+ ctx = await opts.createContext();
160
+ const promises = paths.map((path, index)=>inputToProcedureCall({
161
+ opts,
162
+ ctx,
163
+ type,
164
+ input: inputs[index],
165
+ path
166
+ }));
167
+ if (!isStreamCall) {
168
+ /**
169
+ * Non-streaming response:
170
+ * - await all responses in parallel, blocking on the slowest one
171
+ * - create headers with known response body
172
+ * - return a complete HTTPResponse
173
+ */ const untransformedJSON = await Promise.all(promises);
174
+ const errors = untransformedJSON.flatMap((response)=>'error' in response ? [
175
+ response.error
176
+ ] : []);
177
+ const headResponse1 = initResponse({
178
+ ctx,
179
+ paths,
180
+ type,
181
+ responseMeta: opts.responseMeta,
182
+ untransformedJSON,
183
+ errors
184
+ });
185
+ onHead?.(headResponse1);
186
+ // return body stuff
187
+ 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
188
+ const transformedJSON = transformTRPCResponse(router._def._config, result);
189
+ const body = JSON.stringify(transformedJSON);
190
+ onChunk?.([
191
+ -1,
192
+ body
193
+ ]);
194
+ return {
195
+ status: headResponse1.status,
196
+ headers: headResponse1.headers,
197
+ body
198
+ };
199
+ }
200
+ /**
201
+ * Streaming response:
202
+ * - block on none, call `onChunk` as soon as each response is ready
203
+ * - create headers with minimal data (cannot know the response body in advance)
204
+ * - return void
205
+ */ const headResponse2 = initResponse({
206
+ ctx,
207
+ paths,
208
+ type,
209
+ responseMeta: opts.responseMeta
210
+ });
211
+ onHead(headResponse2);
212
+ const indexedPromises = new Map(promises.map((promise, index)=>[
213
+ index,
214
+ promise.then((r)=>[
215
+ index,
216
+ r
217
+ ])
218
+ ]));
219
+ for(let i = 0; i < paths.length; i++){
220
+ const [index, untransformedJSON1] = await Promise.race(indexedPromises.values());
221
+ indexedPromises.delete(index);
222
+ try {
223
+ const transformedJSON1 = transformTRPCResponse(router._def._config, untransformedJSON1);
224
+ const body1 = JSON.stringify(transformedJSON1);
225
+ onChunk([
226
+ index,
227
+ body1
228
+ ]);
229
+ } catch (cause) {
230
+ const path = paths[index];
231
+ const input = inputs[index];
232
+ const { body: body2 } = caughtErrorToData(cause, {
233
+ opts,
234
+ ctx,
235
+ type,
236
+ path,
237
+ input
238
+ });
239
+ onChunk([
240
+ index,
241
+ body2
242
+ ]);
243
+ }
244
+ }
245
+ return;
246
+ } catch (cause1) {
247
+ // we get here if
248
+ // - batching is called when it's not enabled
249
+ // - `createContext()` throws
250
+ // - `router._def._config.transformer.output.serialize()` throws
251
+ // - post body is too large
252
+ // - input deserialization fails
253
+ // - `errorFormatter` return value is malformed
254
+ const { error , untransformedJSON: untransformedJSON2 , body: body3 } = caughtErrorToData(cause1, {
255
+ opts,
256
+ ctx,
257
+ type
258
+ });
259
+ const headResponse3 = initResponse({
260
+ ctx,
261
+ paths,
262
+ type,
263
+ responseMeta: opts.responseMeta,
264
+ untransformedJSON: untransformedJSON2,
265
+ errors: [
266
+ error
267
+ ]
268
+ });
269
+ onHead?.(headResponse3);
270
+ onChunk?.([
271
+ -1,
272
+ body3
273
+ ]);
274
+ return {
275
+ status: headResponse3.status,
276
+ headers: headResponse3.headers,
277
+ body: body3
278
+ };
279
+ }
280
+ }
281
+
282
+ export { resolveHTTPResponse as r };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trpc/server",
3
- "version": "10.28.2",
3
+ "version": "10.29.0",
4
4
  "description": "The tRPC server library",
5
5
  "author": "KATT",
6
6
  "license": "MIT",
@@ -143,7 +143,7 @@
143
143
  "@types/express": "^4.17.17",
144
144
  "@types/hash-sum": "^1.0.0",
145
145
  "@types/node": "^18.16.16",
146
- "@types/react": "^18.2.6",
146
+ "@types/react": "^18.2.8",
147
147
  "@types/react-dom": "^18.2.4",
148
148
  "@types/ws": "^8.2.0",
149
149
  "@web3-storage/multipart-parser": "^1.0.0",
@@ -164,7 +164,7 @@
164
164
  "superstruct": "^1.0.0",
165
165
  "tslib": "^2.5.0",
166
166
  "tsx": "^3.12.7",
167
- "typescript": "^5.0.4",
167
+ "typescript": "^5.1.3",
168
168
  "vitest": "^0.28.5",
169
169
  "ws": "^8.0.0",
170
170
  "yup": "^1.0.0",
@@ -173,5 +173,5 @@
173
173
  "funding": [
174
174
  "https://trpc.io/sponsor"
175
175
  ],
176
- "gitHead": "055c27724a3163ee9fde633f92f30756caf7690b"
176
+ "gitHead": "49b0a55b74b57bbf7451dad5976d315d9fb7785c"
177
177
  }
@@ -1,6 +1,12 @@
1
+ import { Readable } from 'node:stream';
1
2
  import { FastifyReply, FastifyRequest } from 'fastify';
2
3
  import { AnyRouter, inferRouterContext } from '../../core';
3
- import { HTTPBaseHandlerOptions, HTTPRequest } from '../../http';
4
+ import {
5
+ getBatchStreamFormatter,
6
+ HTTPBaseHandlerOptions,
7
+ HTTPRequest,
8
+ } from '../../http';
9
+ import { HTTPResponse, ResponseChunk } from '../../http/internals/types';
4
10
  import { resolveHTTPResponse } from '../../http/resolveHTTPResponse';
5
11
  import { NodeHTTPCreateContextOption } from '../node-http';
6
12
 
@@ -43,7 +49,48 @@ export async function fastifyRequestHandler<
43
49
  body: opts.req.body ?? 'null',
44
50
  };
45
51
 
46
- const result = await resolveHTTPResponse({
52
+ let resolve: (value: FastifyReply) => void;
53
+ const promise = new Promise<FastifyReply>((r) => (resolve = r));
54
+
55
+ const onHead = (head: HTTPResponse) => {
56
+ if (!opts.res.statusCode || opts.res.statusCode === 200) {
57
+ opts.res.statusCode = head.status;
58
+ }
59
+ for (const [key, value] of Object.entries(head.headers ?? {})) {
60
+ /* istanbul ignore if -- @preserve */
61
+ if (typeof value === 'undefined') {
62
+ continue;
63
+ }
64
+ void opts.res.header(key, value);
65
+ }
66
+ };
67
+
68
+ let isStream = false;
69
+ let stream: Readable;
70
+ const formatter = getBatchStreamFormatter();
71
+ const onChunk = ([index, string]: ResponseChunk) => {
72
+ if (index === -1) {
73
+ // full response, no streaming
74
+ resolve(opts.res.send(string));
75
+ return;
76
+ }
77
+ if (!isStream) {
78
+ void opts.res.header('Transfer-Encoding', 'chunked');
79
+ void opts.res.header(
80
+ 'Vary',
81
+ opts.res.hasHeader('Vary')
82
+ ? 'trpc-batch-mode, ' + opts.res.getHeader('Vary')
83
+ : 'trpc-batch-mode',
84
+ );
85
+ stream = new Readable();
86
+ stream._read = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function -- https://github.com/fastify/fastify/issues/805#issuecomment-369172154
87
+ resolve(opts.res.send(stream));
88
+ isStream = true;
89
+ }
90
+ stream.push(formatter(index, string));
91
+ };
92
+
93
+ resolveHTTPResponse({
47
94
  req,
48
95
  createContext,
49
96
  path: opts.path,
@@ -53,20 +100,20 @@ export async function fastifyRequestHandler<
53
100
  onError(o) {
54
101
  opts?.onError?.({ ...o, req: opts.req });
55
102
  },
56
- });
57
-
58
- const { res } = opts;
59
-
60
- if ('status' in result && (!res.statusCode || res.statusCode === 200)) {
61
- res.statusCode = result.status;
62
- }
63
- for (const [key, value] of Object.entries(result.headers ?? {})) {
64
- /* istanbul ignore if -- @preserve */
65
- if (typeof value === 'undefined') {
66
- continue;
67
- }
103
+ onHead,
104
+ onChunk,
105
+ })
106
+ .then(() => {
107
+ if (isStream) {
108
+ stream.push(formatter.end());
109
+ stream.push(null); // https://github.com/fastify/fastify/issues/805#issuecomment-369172154
110
+ }
111
+ })
112
+ .catch(() => {
113
+ if (isStream) {
114
+ stream.push(null);
115
+ }
116
+ });
68
117
 
69
- void res.header(key, value);
70
- }
71
- await res.send(result.body);
118
+ return promise;
72
119
  }
@@ -1,5 +1,6 @@
1
1
  import { AnyRouter } from '../../core';
2
- import { HTTPRequest } from '../../http';
2
+ import { getBatchStreamFormatter, HTTPRequest } from '../../http';
3
+ import { HTTPResponse, ResponseChunk } from '../../http/internals/types';
3
4
  import { resolveHTTPResponse } from '../../http/resolveHTTPResponse';
4
5
  import { FetchHandlerOptions } from './types';
5
6
 
@@ -29,7 +30,61 @@ export async function fetchRequestHandler<TRouter extends AnyRouter>(
29
30
  : '',
30
31
  };
31
32
 
32
- const result = await resolveHTTPResponse({
33
+ let resolve: (value: Response) => void;
34
+ const promise = new Promise<Response>((r) => (resolve = r));
35
+ let status = 200;
36
+
37
+ const onHead = (head: HTTPResponse) => {
38
+ for (const [key, value] of Object.entries(head.headers ?? {})) {
39
+ /* istanbul ignore if -- @preserve */
40
+ if (typeof value === 'undefined') {
41
+ continue;
42
+ }
43
+ if (typeof value === 'string') {
44
+ resHeaders.set(key, value);
45
+ continue;
46
+ }
47
+ for (const v of value) {
48
+ resHeaders.append(key, v);
49
+ }
50
+ }
51
+ status = head.status;
52
+ };
53
+
54
+ let isStream = false;
55
+ let controller: ReadableStreamController<any>;
56
+ let encoder: TextEncoder;
57
+ const formatter = getBatchStreamFormatter();
58
+ const onChunk = ([index, string]: ResponseChunk) => {
59
+ if (index === -1) {
60
+ // full response, no streaming
61
+ const response = new Response(string || null, {
62
+ status,
63
+ headers: resHeaders,
64
+ });
65
+ resolve(response);
66
+ return;
67
+ }
68
+ if (!isStream) {
69
+ resHeaders.set('Transfer-Encoding', 'chunked');
70
+ resHeaders.append('Vary', 'trpc-batch-mode');
71
+ const stream = new ReadableStream({
72
+ start(c) {
73
+ controller = c;
74
+ },
75
+ });
76
+ const response = new Response(stream, {
77
+ status,
78
+ headers: resHeaders,
79
+ });
80
+ resolve(response);
81
+ encoder = new TextEncoder();
82
+ isStream = true;
83
+ }
84
+ controller.enqueue(encoder.encode(formatter(index, string)));
85
+ };
86
+
87
+ resolveHTTPResponse({
33
88
  req,
34
89
  createContext,
35
90
  path,
@@ -39,28 +94,20 @@ export async function fetchRequestHandler<TRouter extends AnyRouter>(
39
94
  onError(o) {
40
95
  opts?.onError?.({ ...o, req: opts.req });
41
96
  },
42
- });
43
-
44
- for (const [key, value] of Object.entries(result.headers ?? {})) {
45
- /* istanbul ignore if -- @preserve */
46
- if (typeof value === 'undefined') {
47
- continue;
48
- }
49
-
50
- if (typeof value === 'string') {
51
- resHeaders.set(key, value);
52
- continue;
53
- }
54
-
55
- for (const v of value) {
56
- resHeaders.append(key, v);
57
- }
58
- }
59
-
60
- const res = new Response(result.body, {
61
- status: result.status,
62
- headers: resHeaders,
63
- });
97
+ onHead,
98
+ onChunk,
99
+ })
100
+ .then(() => {
101
+ if (isStream) {
102
+ controller.enqueue(encoder.encode(formatter.end()));
103
+ controller.close();
104
+ }
105
+ })
106
+ .catch(() => {
107
+ if (isStream) {
108
+ controller.close();
109
+ }
110
+ });
64
111
 
65
- return res;
112
+ return promise;
66
113
  }
@@ -1,28 +1,24 @@
1
1
  import { Transform, TransformCallback } from 'node:stream';
2
2
 
3
3
  class SliceStream extends Transform {
4
- #start: number;
5
- #end: number;
6
- #offset = 0;
7
- #emitUp = false;
8
- #emitDown = false;
4
+ private indexOffset = 0;
5
+ private emitUp = false;
6
+ private emitDown = false;
9
7
 
10
- constructor(start = 0, end = Infinity) {
8
+ constructor(private startIndex = 0, private endIndex = Infinity) {
11
9
  super();
12
- this.#start = start;
13
- this.#end = end;
14
10
  }
15
11
 
16
12
  _transform(chunk: any, _: BufferEncoding, done: TransformCallback): void {
17
- this.#offset += chunk.length;
13
+ this.indexOffset += chunk.length;
18
14
 
19
- if (!this.#emitUp && this.#offset >= this.#start) {
20
- this.#emitUp = true;
21
- const start = chunk.length - (this.#offset - this.#start);
15
+ if (!this.emitUp && this.indexOffset >= this.startIndex) {
16
+ this.emitUp = true;
17
+ const start = chunk.length - (this.indexOffset - this.startIndex);
22
18
 
23
- if (this.#offset > this.#end) {
24
- const end = chunk.length - (this.#offset - this.#end);
25
- this.#emitDown = true;
19
+ if (this.indexOffset > this.endIndex) {
20
+ const end = chunk.length - (this.indexOffset - this.endIndex);
21
+ this.emitDown = true;
26
22
  this.push(chunk.slice(start, end));
27
23
  } else {
28
24
  this.push(chunk.slice(start, chunk.length));
@@ -31,10 +27,12 @@ class SliceStream extends Transform {
31
27
  return done();
32
28
  }
33
29
 
34
- if (this.#emitUp && !this.#emitDown) {
35
- if (this.#offset >= this.#end) {
36
- this.#emitDown = true;
37
- this.push(chunk.slice(0, chunk.length - (this.#offset - this.#end)));
30
+ if (this.emitUp && !this.emitDown) {
31
+ if (this.indexOffset >= this.endIndex) {
32
+ this.emitDown = true;
33
+ this.push(
34
+ chunk.slice(0, chunk.length - (this.indexOffset - this.endIndex)),
35
+ );
38
36
  } else {
39
37
  this.push(chunk);
40
38
  }
@@ -46,6 +44,6 @@ class SliceStream extends Transform {
46
44
  }
47
45
  }
48
46
 
49
- export function streamSlice(start = 0, end = Infinity): SliceStream {
50
- return new SliceStream(start, end);
47
+ export function streamSlice(startIndex = 0, endIndex = Infinity): SliceStream {
48
+ return new SliceStream(startIndex, endIndex);
51
49
  }
@@ -1,7 +1,8 @@
1
1
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
2
  import { AnyRouter } from '../../core';
3
3
  import { inferRouterContext } from '../../core/types';
4
- import { HTTPRequest } from '../../http';
4
+ import { getBatchStreamFormatter, HTTPRequest } from '../../http';
5
+ import { HTTPResponse, ResponseChunk } from '../../http/internals/types';
5
6
  import { resolveHTTPResponse } from '../../http/resolveHTTPResponse';
6
7
  import { nodeHTTPJSONContentTypeHandler } from './content-type/json';
7
8
  import { NodeHTTPContentTypeHandler } from './internals/contentType';
@@ -63,7 +64,47 @@ export async function nodeHTTPRequestHandler<
63
64
  body: bodyResult.ok ? bodyResult.data : undefined,
64
65
  };
65
66
 
66
- const result = await resolveHTTPResponse({
67
+ const onHead = (head: HTTPResponse) => {
68
+ if (
69
+ 'status' in head &&
70
+ (!opts.res.statusCode || opts.res.statusCode === 200)
71
+ ) {
72
+ opts.res.statusCode = head.status;
73
+ }
74
+ for (const [key, value] of Object.entries(head.headers ?? {})) {
75
+ /* istanbul ignore if -- @preserve */
76
+ if (typeof value === 'undefined') {
77
+ continue;
78
+ }
79
+ opts.res.setHeader(key, value);
80
+ }
81
+ };
82
+
83
+ const formatter = getBatchStreamFormatter();
84
+ let isStream = false;
85
+ const onChunk = ([index, string]: ResponseChunk) => {
86
+ if (index === -1) {
87
+ /**
88
+ * Full response, no streaming. This can happen
89
+ * - if the response is an error
90
+ * - if response is empty (HEAD request)
91
+ */
92
+ opts.res.end(string);
93
+ return;
94
+ }
95
+ if (!isStream) {
96
+ opts.res.setHeader('Transfer-Encoding', 'chunked');
97
+ const vary = opts.res.getHeader('Vary');
98
+ opts.res.setHeader(
99
+ 'Vary',
100
+ vary ? 'trpc-batch-mode, ' + vary : 'trpc-batch-mode',
101
+ );
102
+ isStream = true;
103
+ }
104
+ opts.res.write(formatter(index, string));
105
+ };
106
+
107
+ await resolveHTTPResponse({
67
108
  batching: opts.batching,
68
109
  responseMeta: opts.responseMeta,
69
110
  path: opts.path,
@@ -79,18 +120,16 @@ export async function nodeHTTPRequestHandler<
79
120
  });
80
121
  },
81
122
  contentTypeHandler,
123
+ onHead,
124
+ onChunk,
82
125
  });
83
126
 
84
- const { res } = opts;
85
- if ('status' in result && (!res.statusCode || res.statusCode === 200)) {
86
- res.statusCode = result.status;
127
+ if (!isStream) {
128
+ return opts.res;
87
129
  }
88
- for (const [key, value] of Object.entries(result.headers ?? {})) {
89
- if (typeof value === 'undefined') {
90
- continue;
91
- }
92
- res.setHeader(key, value);
93
- }
94
- res.end(result.body);
130
+
131
+ opts.res.write(formatter.end());
132
+ opts.res.end();
133
+ return opts.res;
95
134
  });
96
135
  }