@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,256 @@
1
+ import { b as callProcedure } from './config-54786004.js';
2
+ import { T as TRPCError, a as getTRPCErrorFromUnknown } from './TRPCError-7c3909ea.js';
3
+ import { t as transformTRPCResponse, g as getErrorShape } from './transformTRPCResponse-7fcbe472.js';
4
+ import { g as getJsonContentTypeInputs } from './contentType-7354963c.js';
5
+ import { b as getHTTPStatusCode } from './index-ca9a128e.js';
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
22
+ ? []
23
+ : Array.isArray(untransformedJSON)
24
+ ? untransformedJSON
25
+ : [untransformedJSON];
26
+ const meta = responseMeta?.({
27
+ ctx,
28
+ paths,
29
+ type,
30
+ data,
31
+ errors,
32
+ eagerGeneration,
33
+ }) ?? {};
34
+ for (const [key, value] of Object.entries(meta.headers ?? {})) {
35
+ headers[key] = value;
36
+ }
37
+ if (meta.status) {
38
+ status = meta.status;
39
+ }
40
+ return {
41
+ status,
42
+ headers,
43
+ };
44
+ }
45
+ async function inputToProcedureCall(procedureOpts) {
46
+ const { opts, ctx, type, input, path } = procedureOpts;
47
+ try {
48
+ const data = await callProcedure({
49
+ procedures: opts.router._def.procedures,
50
+ path,
51
+ rawInput: input,
52
+ ctx,
53
+ type,
54
+ });
55
+ return {
56
+ result: {
57
+ data,
58
+ },
59
+ };
60
+ }
61
+ catch (cause) {
62
+ const error = getTRPCErrorFromUnknown(cause);
63
+ opts.onError?.({ error, path, input, ctx, type: type, req: opts.req });
64
+ return {
65
+ error: getErrorShape({
66
+ config: opts.router._def._config,
67
+ error,
68
+ type,
69
+ path,
70
+ input,
71
+ ctx,
72
+ }),
73
+ };
74
+ }
75
+ }
76
+ function caughtErrorToData(cause, errorOpts) {
77
+ const { router, req, onError } = errorOpts.opts;
78
+ const error = getTRPCErrorFromUnknown(cause);
79
+ onError?.({
80
+ error,
81
+ path: errorOpts.path,
82
+ input: errorOpts.input,
83
+ ctx: errorOpts.ctx,
84
+ type: errorOpts.type,
85
+ req,
86
+ });
87
+ const untransformedJSON = {
88
+ error: getErrorShape({
89
+ config: router._def._config,
90
+ error,
91
+ type: errorOpts.type,
92
+ path: errorOpts.path,
93
+ input: errorOpts.input,
94
+ ctx: errorOpts.ctx,
95
+ }),
96
+ };
97
+ const transformedJSON = transformTRPCResponse(router._def._config, untransformedJSON);
98
+ const body = JSON.stringify(transformedJSON);
99
+ return {
100
+ error,
101
+ untransformedJSON,
102
+ body,
103
+ };
104
+ }
105
+ // implementation
106
+ async function resolveHTTPResponse(opts) {
107
+ const { router, req, unstable_onHead, unstable_onChunk } = opts;
108
+ if (req.method === 'HEAD') {
109
+ // can be used for lambda warmup
110
+ const headResponse = {
111
+ status: 204,
112
+ };
113
+ unstable_onHead?.(headResponse, false);
114
+ unstable_onChunk?.([-1, '']);
115
+ return headResponse;
116
+ }
117
+ const contentTypeHandler = opts.contentTypeHandler ?? fallbackContentTypeHandler;
118
+ const batchingEnabled = opts.batching?.enabled ?? true;
119
+ const type = HTTP_METHOD_PROCEDURE_TYPE_MAP[req.method] ?? 'unknown';
120
+ let ctx = undefined;
121
+ let paths;
122
+ const isBatchCall = !!req.query.get('batch');
123
+ const isStreamCall = isBatchCall &&
124
+ unstable_onHead &&
125
+ unstable_onChunk &&
126
+ req.headers['trpc-batch-mode'] === 'stream';
127
+ try {
128
+ if (opts.error) {
129
+ throw opts.error;
130
+ }
131
+ if (isBatchCall && !batchingEnabled) {
132
+ throw new Error(`Batching is not enabled on the server`);
133
+ }
134
+ /* istanbul ignore if -- @preserve */
135
+ if (type === 'subscription') {
136
+ throw new TRPCError({
137
+ message: 'Subscriptions should use wsLink',
138
+ code: 'METHOD_NOT_SUPPORTED',
139
+ });
140
+ }
141
+ if (type === 'unknown') {
142
+ throw new TRPCError({
143
+ message: `Unexpected request method ${req.method}`,
144
+ code: 'METHOD_NOT_SUPPORTED',
145
+ });
146
+ }
147
+ const inputs = await contentTypeHandler.getInputs({
148
+ isBatchCall,
149
+ req,
150
+ router,
151
+ preprocessedBody: opts.preprocessedBody ?? false,
152
+ });
153
+ paths = isBatchCall ? opts.path.split(',') : [opts.path];
154
+ ctx = await opts.createContext();
155
+ const promises = paths.map((path, index) => inputToProcedureCall({ opts, ctx, type, input: inputs[index], path }));
156
+ if (!isStreamCall) {
157
+ /**
158
+ * Non-streaming response:
159
+ * - await all responses in parallel, blocking on the slowest one
160
+ * - create headers with known response body
161
+ * - return a complete HTTPResponse
162
+ */
163
+ const untransformedJSON = await Promise.all(promises);
164
+ const errors = untransformedJSON.flatMap((response) => 'error' in response ? [response.error] : []);
165
+ const headResponse = initResponse({
166
+ ctx,
167
+ paths,
168
+ type,
169
+ responseMeta: opts.responseMeta,
170
+ untransformedJSON,
171
+ errors,
172
+ });
173
+ unstable_onHead?.(headResponse, false);
174
+ // return body stuff
175
+ 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
176
+ const transformedJSON = transformTRPCResponse(router._def._config, result);
177
+ const body = JSON.stringify(transformedJSON);
178
+ unstable_onChunk?.([-1, body]);
179
+ return {
180
+ status: headResponse.status,
181
+ headers: headResponse.headers,
182
+ body,
183
+ };
184
+ }
185
+ /**
186
+ * Streaming response:
187
+ * - block on none, call `onChunk` as soon as each response is ready
188
+ * - create headers with minimal data (cannot know the response body in advance)
189
+ * - return void
190
+ */
191
+ const headResponse = initResponse({
192
+ ctx,
193
+ paths,
194
+ type,
195
+ responseMeta: opts.responseMeta,
196
+ });
197
+ unstable_onHead(headResponse, true);
198
+ const indexedPromises = new Map(promises.map((promise, index) => [
199
+ index,
200
+ promise.then((r) => [index, r]),
201
+ ]));
202
+ for (let i = 0; i < paths.length; i++) {
203
+ const [index, untransformedJSON] = await Promise.race(indexedPromises.values());
204
+ indexedPromises.delete(index);
205
+ try {
206
+ const transformedJSON = transformTRPCResponse(router._def._config, untransformedJSON);
207
+ const body = JSON.stringify(transformedJSON);
208
+ unstable_onChunk([index, body]);
209
+ }
210
+ catch (cause) {
211
+ const path = paths[index];
212
+ const input = inputs[index];
213
+ const { body } = caughtErrorToData(cause, {
214
+ opts,
215
+ ctx,
216
+ type,
217
+ path,
218
+ input,
219
+ });
220
+ unstable_onChunk([index, body]);
221
+ }
222
+ }
223
+ return;
224
+ }
225
+ catch (cause) {
226
+ // we get here if
227
+ // - batching is called when it's not enabled
228
+ // - `createContext()` throws
229
+ // - `router._def._config.transformer.output.serialize()` throws
230
+ // - post body is too large
231
+ // - input deserialization fails
232
+ // - `errorFormatter` return value is malformed
233
+ const { error, untransformedJSON, body } = caughtErrorToData(cause, {
234
+ opts,
235
+ ctx,
236
+ type,
237
+ });
238
+ const headResponse = initResponse({
239
+ ctx,
240
+ paths,
241
+ type,
242
+ responseMeta: opts.responseMeta,
243
+ untransformedJSON,
244
+ errors: [error],
245
+ });
246
+ unstable_onHead?.(headResponse, false);
247
+ unstable_onChunk?.([-1, body]);
248
+ return {
249
+ status: headResponse.status,
250
+ headers: headResponse.headers,
251
+ body,
252
+ };
253
+ }
254
+ }
255
+
256
+ 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.1",
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": "3dbc7b7e9fff233f89627c21450ba89754bc3e46"
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,49 @@ 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
+ let isStream = false;
56
+ let stream: Readable;
57
+ let formatter: ReturnType<typeof getBatchStreamFormatter>;
58
+ const unstable_onHead = (head: HTTPResponse, isStreaming: boolean) => {
59
+ if (!opts.res.statusCode || opts.res.statusCode === 200) {
60
+ opts.res.statusCode = head.status;
61
+ }
62
+ for (const [key, value] of Object.entries(head.headers ?? {})) {
63
+ /* istanbul ignore if -- @preserve */
64
+ if (typeof value === 'undefined') {
65
+ continue;
66
+ }
67
+ void opts.res.header(key, value);
68
+ }
69
+ if (isStreaming) {
70
+ void opts.res.header('Transfer-Encoding', 'chunked');
71
+ void opts.res.header(
72
+ 'Vary',
73
+ opts.res.hasHeader('Vary')
74
+ ? 'trpc-batch-mode, ' + opts.res.getHeader('Vary')
75
+ : 'trpc-batch-mode',
76
+ );
77
+ stream = new Readable();
78
+ stream._read = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function -- https://github.com/fastify/fastify/issues/805#issuecomment-369172154
79
+ resolve(opts.res.send(stream));
80
+ isStream = true;
81
+ formatter = getBatchStreamFormatter();
82
+ }
83
+ };
84
+
85
+ const unstable_onChunk = ([index, string]: ResponseChunk) => {
86
+ if (index === -1) {
87
+ // full response, no streaming
88
+ resolve(opts.res.send(string));
89
+ } else {
90
+ stream.push(formatter!(index, string));
91
+ }
92
+ };
93
+
94
+ resolveHTTPResponse({
47
95
  req,
48
96
  createContext,
49
97
  path: opts.path,
@@ -53,20 +101,20 @@ export async function fastifyRequestHandler<
53
101
  onError(o) {
54
102
  opts?.onError?.({ ...o, req: opts.req });
55
103
  },
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
- }
104
+ unstable_onHead,
105
+ unstable_onChunk,
106
+ })
107
+ .then(() => {
108
+ if (isStream) {
109
+ stream.push(formatter!.end());
110
+ stream.push(null); // https://github.com/fastify/fastify/issues/805#issuecomment-369172154
111
+ }
112
+ })
113
+ .catch(() => {
114
+ if (isStream) {
115
+ stream.push(null);
116
+ }
117
+ });
68
118
 
69
- void res.header(key, value);
70
- }
71
- await res.send(result.body);
119
+ return promise;
72
120
  }
@@ -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,62 @@ 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
+ let isStream = false;
38
+ let controller: ReadableStreamController<any>;
39
+ let encoder: TextEncoder;
40
+ let formatter: ReturnType<typeof getBatchStreamFormatter>;
41
+ const unstable_onHead = (head: HTTPResponse, isStreaming: boolean) => {
42
+ for (const [key, value] of Object.entries(head.headers ?? {})) {
43
+ /* istanbul ignore if -- @preserve */
44
+ if (typeof value === 'undefined') {
45
+ continue;
46
+ }
47
+ if (typeof value === 'string') {
48
+ resHeaders.set(key, value);
49
+ continue;
50
+ }
51
+ for (const v of value) {
52
+ resHeaders.append(key, v);
53
+ }
54
+ }
55
+ status = head.status;
56
+ if (isStreaming) {
57
+ resHeaders.set('Transfer-Encoding', 'chunked');
58
+ resHeaders.append('Vary', 'trpc-batch-mode');
59
+ const stream = new ReadableStream({
60
+ start(c) {
61
+ controller = c;
62
+ },
63
+ });
64
+ const response = new Response(stream, {
65
+ status,
66
+ headers: resHeaders,
67
+ });
68
+ resolve(response);
69
+ encoder = new TextEncoder();
70
+ formatter = getBatchStreamFormatter();
71
+ isStream = true;
72
+ }
73
+ };
74
+
75
+ const unstable_onChunk = ([index, string]: ResponseChunk) => {
76
+ if (index === -1) {
77
+ // full response, no streaming
78
+ const response = new Response(string || null, {
79
+ status,
80
+ headers: resHeaders,
81
+ });
82
+ resolve(response);
83
+ } else {
84
+ controller.enqueue(encoder.encode(formatter!(index, string)));
85
+ }
86
+ };
87
+
88
+ resolveHTTPResponse({
33
89
  req,
34
90
  createContext,
35
91
  path,
@@ -39,28 +95,20 @@ export async function fetchRequestHandler<TRouter extends AnyRouter>(
39
95
  onError(o) {
40
96
  opts?.onError?.({ ...o, req: opts.req });
41
97
  },
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
- });
98
+ unstable_onHead,
99
+ unstable_onChunk,
100
+ })
101
+ .then(() => {
102
+ if (isStream) {
103
+ controller.enqueue(encoder.encode(formatter!.end()));
104
+ controller.close();
105
+ }
106
+ })
107
+ .catch(() => {
108
+ if (isStream) {
109
+ controller.close();
110
+ }
111
+ });
64
112
 
65
- return res;
113
+ return promise;
66
114
  }
@@ -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,50 @@ export async function nodeHTTPRequestHandler<
63
64
  body: bodyResult.ok ? bodyResult.data : undefined,
64
65
  };
65
66
 
66
- const result = await resolveHTTPResponse({
67
+ let isStream = false;
68
+ let formatter: ReturnType<typeof getBatchStreamFormatter>;
69
+ const unstable_onHead = (head: HTTPResponse, isStreaming: boolean) => {
70
+ if (
71
+ 'status' in head &&
72
+ (!opts.res.statusCode || opts.res.statusCode === 200)
73
+ ) {
74
+ opts.res.statusCode = head.status;
75
+ }
76
+ for (const [key, value] of Object.entries(head.headers ?? {})) {
77
+ /* istanbul ignore if -- @preserve */
78
+ if (typeof value === 'undefined') {
79
+ continue;
80
+ }
81
+ opts.res.setHeader(key, value);
82
+ }
83
+ if (isStreaming) {
84
+ opts.res.setHeader('Transfer-Encoding', 'chunked');
85
+ const vary = opts.res.getHeader('Vary');
86
+ opts.res.setHeader(
87
+ 'Vary',
88
+ vary ? 'trpc-batch-mode, ' + vary : 'trpc-batch-mode',
89
+ );
90
+ isStream = true;
91
+ formatter = getBatchStreamFormatter();
92
+ opts.res.flushHeaders();
93
+ }
94
+ };
95
+
96
+ const unstable_onChunk = ([index, string]: ResponseChunk) => {
97
+ if (index === -1) {
98
+ /**
99
+ * Full response, no streaming. This can happen
100
+ * - if the response is an error
101
+ * - if response is empty (HEAD request)
102
+ */
103
+ opts.res.end(string);
104
+ } else {
105
+ opts.res.write(formatter!(index, string));
106
+ opts.res.flush?.();
107
+ }
108
+ };
109
+
110
+ await resolveHTTPResponse({
67
111
  batching: opts.batching,
68
112
  responseMeta: opts.responseMeta,
69
113
  path: opts.path,
@@ -79,18 +123,15 @@ export async function nodeHTTPRequestHandler<
79
123
  });
80
124
  },
81
125
  contentTypeHandler,
126
+ unstable_onHead,
127
+ unstable_onChunk,
82
128
  });
83
129
 
84
- const { res } = opts;
85
- if ('status' in result && (!res.statusCode || res.statusCode === 200)) {
86
- res.statusCode = result.status;
130
+ if (isStream) {
131
+ opts.res.write(formatter!.end());
132
+ opts.res.end();
87
133
  }
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);
134
+
135
+ return opts.res;
95
136
  });
96
137
  }
@@ -12,7 +12,17 @@ export type NodeHTTPRequest = IncomingMessage & {
12
12
  query?: ParsedQs;
13
13
  body?: unknown;
14
14
  };
15
- export type NodeHTTPResponse = ServerResponse;
15
+ export type NodeHTTPResponse = ServerResponse & {
16
+ /**
17
+ * Force the partially-compressed response to be flushed to the client.
18
+ *
19
+ * Added by compression middleware
20
+ * (depending on the environment,
21
+ * e.g. Next <= 12,
22
+ * e.g. Express w/ `compression()`)
23
+ */
24
+ flush?: () => void;
25
+ };
16
26
 
17
27
  export type NodeHTTPCreateContextOption<
18
28
  TRouter extends AnyRouter,