@trpc/client 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 (49) hide show
  1. package/dist/httpBatchLink-38dcf5f2.js +243 -0
  2. package/dist/httpBatchLink-e66c0674.mjs +240 -0
  3. package/dist/httpBatchLink-f35df745.js +239 -0
  4. package/dist/{httpUtils-00c8b54e.js → httpUtils-145e783b.js} +27 -23
  5. package/dist/{httpUtils-90a6bccb.js → httpUtils-21cbb35d.js} +27 -20
  6. package/dist/{httpUtils-8a5c637a.mjs → httpUtils-b2fcc20c.mjs} +26 -21
  7. package/dist/index.js +162 -2
  8. package/dist/index.mjs +164 -4
  9. package/dist/internals/dataLoader.d.ts +1 -1
  10. package/dist/internals/dataLoader.d.ts.map +1 -1
  11. package/dist/internals/types.d.ts +2 -0
  12. package/dist/internals/types.d.ts.map +1 -1
  13. package/dist/links/HTTPBatchLinkOptions.d.ts +20 -0
  14. package/dist/links/HTTPBatchLinkOptions.d.ts.map +1 -0
  15. package/dist/links/HTTPBatchStreamLinkOptions.d.ts +11 -0
  16. package/dist/links/HTTPBatchStreamLinkOptions.d.ts.map +1 -0
  17. package/dist/links/httpBatchLink.d.ts +2 -15
  18. package/dist/links/httpBatchLink.d.ts.map +1 -1
  19. package/dist/links/httpBatchLink.js +5 -217
  20. package/dist/links/httpBatchLink.mjs +4 -220
  21. package/dist/links/httpBatchStreamLink.d.ts +3 -0
  22. package/dist/links/httpBatchStreamLink.d.ts.map +1 -0
  23. package/dist/links/httpLink.js +1 -1
  24. package/dist/links/httpLink.mjs +1 -1
  25. package/dist/links/index.d.ts +3 -0
  26. package/dist/links/index.d.ts.map +1 -1
  27. package/dist/links/internals/createHTTPBatchLink.d.ts +20 -0
  28. package/dist/links/internals/createHTTPBatchLink.d.ts.map +1 -0
  29. package/dist/links/internals/getTextDecoder.d.ts +3 -0
  30. package/dist/links/internals/getTextDecoder.d.ts.map +1 -0
  31. package/dist/links/internals/httpUtils.d.ts +5 -1
  32. package/dist/links/internals/httpUtils.d.ts.map +1 -1
  33. package/dist/links/internals/parseJSONStream.d.ts +39 -0
  34. package/dist/links/internals/parseJSONStream.d.ts.map +1 -0
  35. package/dist/links/internals/streamingUtils.d.ts +7 -0
  36. package/dist/links/internals/streamingUtils.d.ts.map +1 -0
  37. package/package.json +4 -4
  38. package/src/internals/dataLoader.ts +21 -7
  39. package/src/internals/types.ts +1 -0
  40. package/src/links/HTTPBatchLinkOptions.ts +21 -0
  41. package/src/links/HTTPBatchStreamLinkOptions.ts +11 -0
  42. package/src/links/httpBatchLink.ts +45 -133
  43. package/src/links/httpBatchStreamLink.ts +53 -0
  44. package/src/links/index.ts +3 -0
  45. package/src/links/internals/createHTTPBatchLink.ts +115 -0
  46. package/src/links/internals/getTextDecoder.ts +19 -0
  47. package/src/links/internals/httpUtils.ts +35 -24
  48. package/src/links/internals/parseJSONStream.ts +168 -0
  49. package/src/links/internals/streamingUtils.ts +6 -0
@@ -1,138 +1,50 @@
1
- import { AnyRouter, ProcedureType } from '@trpc/server';
2
- import { observable } from '@trpc/server/observable';
3
- import { dataLoader } from '../internals/dataLoader';
4
1
  import { NonEmptyArray } from '../internals/types';
5
- import { transformResult } from '../shared/transformResult';
6
- import { TRPCClientError } from '../TRPCClientError';
2
+ import { HTTPBatchLinkOptions } from './HTTPBatchLinkOptions';
7
3
  import {
8
- getUrl,
9
- HTTPLinkBaseOptions,
10
- HTTPResult,
11
- jsonHttpRequester,
12
- resolveHTTPLinkOptions,
13
- } from './internals/httpUtils';
14
- import { HTTPHeaders, Operation, TRPCLink } from './types';
15
-
16
- export interface HttpBatchLinkOptions extends HTTPLinkBaseOptions {
17
- maxURLLength?: number;
18
- /**
19
- * Headers to be set on outgoing requests or a callback that of said headers
20
- * @link http://trpc.io/docs/client/headers
21
- */
22
- headers?:
23
- | HTTPHeaders
24
- | ((opts: {
25
- opList: NonEmptyArray<Operation>;
26
- }) => HTTPHeaders | Promise<HTTPHeaders>);
27
- }
28
-
29
- export function httpBatchLink<TRouter extends AnyRouter>(
30
- opts: HttpBatchLinkOptions,
31
- ): TRPCLink<TRouter> {
32
- const resolvedOpts = resolveHTTPLinkOptions(opts);
33
- // initialized config
34
- return (runtime) => {
35
- const maxURLLength = opts.maxURLLength || Infinity;
36
-
37
- const batchLoader = (type: ProcedureType) => {
38
- const validate = (batchOps: Operation[]) => {
39
- if (maxURLLength === Infinity) {
40
- // escape hatch for quick calcs
41
- return true;
4
+ createHTTPBatchLink,
5
+ RequesterFn,
6
+ } from './internals/createHTTPBatchLink';
7
+ import { jsonHttpRequester } from './internals/httpUtils';
8
+ import { Operation } from './types';
9
+
10
+ const batchRequester: RequesterFn<HTTPBatchLinkOptions> = (requesterOpts) => {
11
+ return (batchOps) => {
12
+ const path = batchOps.map((op) => op.path).join(',');
13
+ const inputs = batchOps.map((op) => op.input);
14
+
15
+ const { promise, cancel } = jsonHttpRequester({
16
+ ...requesterOpts,
17
+ path,
18
+ inputs,
19
+ headers() {
20
+ if (!requesterOpts.opts.headers) {
21
+ return {};
42
22
  }
43
- const path = batchOps.map((op) => op.path).join(',');
44
- const inputs = batchOps.map((op) => op.input);
45
-
46
- const url = getUrl({
47
- ...resolvedOpts,
48
- runtime,
49
- type,
50
- path,
51
- inputs,
52
- });
53
-
54
- return url.length <= maxURLLength;
55
- };
56
-
57
- const fetch = (batchOps: Operation[]) => {
58
- const path = batchOps.map((op) => op.path).join(',');
59
- const inputs = batchOps.map((op) => op.input);
60
-
61
- const { promise, cancel } = jsonHttpRequester({
62
- ...resolvedOpts,
63
- runtime,
64
- type,
65
- path,
66
- inputs,
67
- headers() {
68
- if (!opts.headers) {
69
- return {};
70
- }
71
- if (typeof opts.headers === 'function') {
72
- return opts.headers({
73
- opList: batchOps as NonEmptyArray<Operation>,
74
- });
75
- }
76
- return opts.headers;
77
- },
78
- });
79
-
80
- return {
81
- promise: promise.then((res) => {
82
- const resJSON = Array.isArray(res.json)
83
- ? res.json
84
- : batchOps.map(() => res.json);
85
-
86
- const result = resJSON.map((item) => ({
87
- meta: res.meta,
88
- json: item,
89
- }));
90
-
91
- return result;
92
- }),
93
- cancel,
94
- };
95
- };
96
-
97
- return { validate, fetch };
98
- };
99
-
100
- const query = dataLoader<Operation, HTTPResult>(batchLoader('query'));
101
- const mutation = dataLoader<Operation, HTTPResult>(batchLoader('mutation'));
102
- const subscription = dataLoader<Operation, HTTPResult>(
103
- batchLoader('subscription'),
104
- );
105
-
106
- const loaders = { query, subscription, mutation };
107
- return ({ op }) => {
108
- return observable((observer) => {
109
- const loader = loaders[op.type];
110
- const { promise, cancel } = loader.load(op);
111
-
112
- promise
113
- .then((res) => {
114
- const transformed = transformResult(res.json, runtime);
115
-
116
- if (!transformed.ok) {
117
- observer.error(
118
- TRPCClientError.from(transformed.error, {
119
- meta: res.meta,
120
- }),
121
- );
122
- return;
123
- }
124
- observer.next({
125
- context: res.meta,
126
- result: transformed.result,
127
- });
128
- observer.complete();
129
- })
130
- .catch((err) => observer.error(TRPCClientError.from(err)));
131
-
132
- return () => {
133
- cancel();
134
- };
135
- });
23
+ if (typeof requesterOpts.opts.headers === 'function') {
24
+ return requesterOpts.opts.headers({
25
+ opList: batchOps as NonEmptyArray<Operation>,
26
+ });
27
+ }
28
+ return requesterOpts.opts.headers;
29
+ },
30
+ });
31
+
32
+ return {
33
+ promise: promise.then((res) => {
34
+ const resJSON = Array.isArray(res.json)
35
+ ? res.json
36
+ : batchOps.map(() => res.json);
37
+
38
+ const result = resJSON.map((item) => ({
39
+ meta: res.meta,
40
+ json: item,
41
+ }));
42
+
43
+ return result;
44
+ }),
45
+ cancel,
136
46
  };
137
47
  };
138
- }
48
+ };
49
+
50
+ export const httpBatchLink = createHTTPBatchLink(batchRequester);
@@ -0,0 +1,53 @@
1
+ import { NonEmptyArray } from '../internals/types';
2
+ import { HTTPBatchStreamLinkOptions } from './HTTPBatchStreamLinkOptions';
3
+ import {
4
+ createHTTPBatchLink,
5
+ RequesterFn,
6
+ } from './internals/createHTTPBatchLink';
7
+ import { getTextDecoder } from './internals/getTextDecoder';
8
+ import { streamingJsonHttpRequester } from './internals/parseJSONStream';
9
+ import { Operation } from './types';
10
+
11
+ const streamRequester: RequesterFn<HTTPBatchStreamLinkOptions> = (
12
+ requesterOpts,
13
+ ) => {
14
+ const textDecoder = getTextDecoder(requesterOpts.opts.textDecoder);
15
+ return (batchOps, unitResolver) => {
16
+ const path = batchOps.map((op) => op.path).join(',');
17
+ const inputs = batchOps.map((op) => op.input);
18
+
19
+ const { cancel, promise } = streamingJsonHttpRequester(
20
+ {
21
+ ...requesterOpts,
22
+ textDecoder,
23
+ path,
24
+ inputs,
25
+ headers() {
26
+ if (!requesterOpts.opts.headers) {
27
+ return {};
28
+ }
29
+ if (typeof requesterOpts.opts.headers === 'function') {
30
+ return requesterOpts.opts.headers({
31
+ opList: batchOps as NonEmptyArray<Operation>,
32
+ });
33
+ }
34
+ return requesterOpts.opts.headers;
35
+ },
36
+ },
37
+ (index, res) => unitResolver(index, res),
38
+ );
39
+
40
+ return {
41
+ /**
42
+ * return an empty array because the batchLoader expects an array of results
43
+ * but we've already called the `unitResolver` for each of them, there's
44
+ * nothing left to do here.
45
+ */
46
+ promise: promise.then(() => []),
47
+ cancel,
48
+ };
49
+ };
50
+ };
51
+
52
+ export const unstable_httpBatchStreamLink =
53
+ createHTTPBatchLink(streamRequester);
@@ -1,6 +1,9 @@
1
1
  export * from './types';
2
2
 
3
3
  export * from './httpBatchLink';
4
+ export * from './httpBatchStreamLink';
5
+ export * from './HTTPBatchLinkOptions';
6
+ export * from './HTTPBatchStreamLinkOptions';
4
7
  export * from './httpLink';
5
8
  export * from './loggerLink';
6
9
  export * from './splitLink';
@@ -0,0 +1,115 @@
1
+ import { AnyRouter, ProcedureType } from '@trpc/server';
2
+ import { observable } from '@trpc/server/observable';
3
+ import { dataLoader } from '../../internals/dataLoader';
4
+ import { transformResult } from '../../shared/transformResult';
5
+ import { TRPCClientError } from '../../TRPCClientError';
6
+ import { HTTPBatchLinkOptions } from '../HTTPBatchLinkOptions';
7
+ import { CancelFn, Operation, TRPCClientRuntime, TRPCLink } from '../types';
8
+ import {
9
+ getUrl,
10
+ HTTPResult,
11
+ ResolvedHTTPLinkOptions,
12
+ resolveHTTPLinkOptions,
13
+ } from './httpUtils';
14
+
15
+ /**
16
+ * @internal
17
+ */
18
+ export type RequesterFn<TOptions extends HTTPBatchLinkOptions> = (
19
+ requesterOpts: ResolvedHTTPLinkOptions & {
20
+ runtime: TRPCClientRuntime;
21
+ type: ProcedureType;
22
+ opts: TOptions;
23
+ },
24
+ ) => (
25
+ batchOps: Operation[],
26
+ unitResolver: (index: number, value: NonNullable<HTTPResult>) => void,
27
+ ) => {
28
+ promise: Promise<HTTPResult[]>;
29
+ cancel: CancelFn;
30
+ };
31
+
32
+ /**
33
+ * @internal
34
+ */
35
+ export function createHTTPBatchLink<TOptions extends HTTPBatchLinkOptions>(
36
+ requester: RequesterFn<TOptions>,
37
+ ) {
38
+ return function httpBatchLink<TRouter extends AnyRouter>(
39
+ opts: TOptions,
40
+ ): TRPCLink<TRouter> {
41
+ const resolvedOpts = resolveHTTPLinkOptions(opts);
42
+ const maxURLLength = opts.maxURLLength || Infinity;
43
+
44
+ // initialized config
45
+ return (runtime) => {
46
+ const batchLoader = (type: ProcedureType) => {
47
+ const validate = (batchOps: Operation[]) => {
48
+ if (maxURLLength === Infinity) {
49
+ // escape hatch for quick calcs
50
+ return true;
51
+ }
52
+ const path = batchOps.map((op) => op.path).join(',');
53
+ const inputs = batchOps.map((op) => op.input);
54
+
55
+ const url = getUrl({
56
+ ...resolvedOpts,
57
+ runtime,
58
+ type,
59
+ path,
60
+ inputs,
61
+ });
62
+
63
+ return url.length <= maxURLLength;
64
+ };
65
+
66
+ const fetch = requester({
67
+ ...resolvedOpts,
68
+ runtime,
69
+ type,
70
+ opts,
71
+ });
72
+
73
+ return { validate, fetch };
74
+ };
75
+
76
+ const query = dataLoader<Operation, HTTPResult>(batchLoader('query'));
77
+ const mutation = dataLoader<Operation, HTTPResult>(
78
+ batchLoader('mutation'),
79
+ );
80
+ const subscription = dataLoader<Operation, HTTPResult>(
81
+ batchLoader('subscription'),
82
+ );
83
+
84
+ const loaders = { query, subscription, mutation };
85
+ return ({ op }) => {
86
+ return observable((observer) => {
87
+ const loader = loaders[op.type];
88
+ const { promise, cancel } = loader.load(op);
89
+
90
+ promise
91
+ .then((res) => {
92
+ const transformed = transformResult(res.json, runtime);
93
+
94
+ if (!transformed.ok) {
95
+ observer.error(
96
+ TRPCClientError.from(transformed.error, {
97
+ meta: res.meta,
98
+ }),
99
+ );
100
+ return;
101
+ }
102
+ observer.next({
103
+ context: res.meta,
104
+ result: transformed.result,
105
+ });
106
+ observer.complete();
107
+ })
108
+ .catch((err) => observer.error(TRPCClientError.from(err)));
109
+
110
+ return () => cancel();
111
+ });
112
+ };
113
+ };
114
+ };
115
+ }
@@ -0,0 +1,19 @@
1
+ import { TextDecoderEsque } from './streamingUtils';
2
+
3
+ export function getTextDecoder(
4
+ customTextDecoder?: TextDecoderEsque,
5
+ ): TextDecoderEsque {
6
+ if (customTextDecoder) {
7
+ return customTextDecoder;
8
+ }
9
+
10
+ if (typeof window !== 'undefined' && window.TextDecoder) {
11
+ return new window.TextDecoder();
12
+ }
13
+
14
+ if (typeof globalThis !== 'undefined' && globalThis.TextDecoder) {
15
+ return new globalThis.TextDecoder();
16
+ }
17
+
18
+ throw new Error('No TextDecoder implementation found');
19
+ }
@@ -4,10 +4,12 @@ import { getFetch } from '../../getFetch';
4
4
  import { getAbortController } from '../../internals/getAbortController';
5
5
  import {
6
6
  AbortControllerEsque,
7
+ AbortControllerInstanceEsque,
7
8
  FetchEsque,
8
9
  RequestInitEsque,
9
10
  ResponseEsque,
10
11
  } from '../../internals/types';
12
+ import { TextDecoderEsque } from '../internals/streamingUtils';
11
13
  import { HTTPHeaders, PromiseAndCancel, TRPCClientRuntime } from '../types';
12
14
 
13
15
  /**
@@ -87,6 +89,7 @@ export type GetBody = (
87
89
  ) => RequestInitEsque['body'];
88
90
 
89
91
  export type ContentOptions = {
92
+ batchModeHeader?: 'stream';
90
93
  contentTypeHeader?: string;
91
94
  getUrl: GetUrl;
92
95
  getBody: GetBody;
@@ -136,38 +139,46 @@ export const jsonHttpRequester: Requester = (opts) => {
136
139
  export type HTTPRequestOptions = HTTPBaseRequestOptions &
137
140
  ContentOptions & {
138
141
  headers: () => HTTPHeaders | Promise<HTTPHeaders>;
142
+ TextDecoder?: TextDecoderEsque;
139
143
  };
140
144
 
145
+ export async function fetchHTTPResponse(
146
+ opts: HTTPRequestOptions,
147
+ ac?: AbortControllerInstanceEsque | null,
148
+ ) {
149
+ const url = opts.getUrl(opts);
150
+ const body = opts.getBody(opts);
151
+ const { type } = opts;
152
+ const headers = await opts.headers();
153
+ /* istanbul ignore if -- @preserve */
154
+ if (type === 'subscription') {
155
+ throw new Error('Subscriptions should use wsLink');
156
+ }
157
+
158
+ return opts.fetch(url, {
159
+ method: METHOD[type],
160
+ signal: ac?.signal,
161
+ body: body,
162
+ headers: {
163
+ ...(opts.contentTypeHeader
164
+ ? { 'content-type': opts.contentTypeHeader }
165
+ : {}),
166
+ ...(opts.batchModeHeader
167
+ ? { 'trpc-batch-mode': opts.batchModeHeader }
168
+ : {}),
169
+ ...headers,
170
+ },
171
+ });
172
+ }
173
+
141
174
  export function httpRequest(
142
175
  opts: HTTPRequestOptions,
143
176
  ): PromiseAndCancel<HTTPResult> {
144
- const { type } = opts;
145
177
  const ac = opts.AbortController ? new opts.AbortController() : null;
178
+ const meta = {} as HTTPResult['meta'];
146
179
 
147
180
  const promise = new Promise<HTTPResult>((resolve, reject) => {
148
- const url = opts.getUrl(opts);
149
- const body = opts.getBody(opts);
150
-
151
- const meta = {} as HTTPResult['meta'];
152
- Promise.resolve(opts.headers())
153
- .then((headers) => {
154
- /* istanbul ignore if -- @preserve */
155
- if (type === 'subscription') {
156
- throw new Error('Subscriptions should use wsLink');
157
- }
158
-
159
- return opts.fetch(url, {
160
- method: METHOD[type],
161
- signal: ac?.signal,
162
- body: body,
163
- headers: {
164
- ...(opts.contentTypeHeader
165
- ? { 'content-type': opts.contentTypeHeader }
166
- : {}),
167
- ...headers,
168
- },
169
- });
170
- })
181
+ fetchHTTPResponse(opts, ac)
171
182
  .then((_res) => {
172
183
  meta.response = _res;
173
184
  return _res.json();
@@ -0,0 +1,168 @@
1
+ // Stream parsing adapted from https://www.loginradius.com/blog/engineering/guest-post/http-streaming-with-nodejs-and-fetch-api/
2
+
3
+ import { TRPCResponse } from '@trpc/server/rpc';
4
+ import { HTTPHeaders } from '../types';
5
+ import {
6
+ fetchHTTPResponse,
7
+ getBody,
8
+ getUrl,
9
+ HTTPBaseRequestOptions,
10
+ HTTPResult,
11
+ } from './httpUtils';
12
+ import { TextDecoderEsque } from './streamingUtils';
13
+
14
+ /**
15
+ * @internal
16
+ * @description Take a stream of bytes and call `onLine` with
17
+ * a JSON object for each line in the stream. Expected stream
18
+ * format is:
19
+ * ```json
20
+ * {"1": {...}
21
+ * ,"0": {...}
22
+ * }
23
+ * ```
24
+ */
25
+ export async function parseJSONStream<TReturn>(opts: {
26
+ /**
27
+ * As given by `(await fetch(url)).body`
28
+ */
29
+ readableStream: ReadableStream<Uint8Array> | NodeJS.ReadableStream;
30
+ /**
31
+ * Called for each line of the stream
32
+ */
33
+ onSingle: (index: number, res: TReturn) => void;
34
+ /**
35
+ * Transform text into useable data object (defaults to JSON.parse)
36
+ */
37
+ parse?: (text: string) => TReturn;
38
+ signal?: AbortSignal;
39
+ textDecoder: TextDecoderEsque;
40
+ }): Promise<void> {
41
+ const parse = opts.parse ?? JSON.parse;
42
+
43
+ const onLine = (line: string) => {
44
+ if (opts.signal?.aborted) return;
45
+ if (!line || line === '}') {
46
+ return;
47
+ }
48
+ /**
49
+ * At this point, `line` can be one of two things:
50
+ * - The first line of the stream `{"2":{...}`
51
+ * - A line in the middle of the stream `,"2":{...}`
52
+ */
53
+ const indexOfColon = line.indexOf(':');
54
+ const indexAsStr = line.substring(2, indexOfColon - 1);
55
+ const text = line.substring(indexOfColon + 1);
56
+
57
+ opts.onSingle(Number(indexAsStr), parse(text));
58
+ };
59
+
60
+ await readLines(opts.readableStream, onLine, opts.textDecoder);
61
+ }
62
+
63
+ /**
64
+ * Handle transforming a stream of bytes into lines of text.
65
+ * To avoid using AsyncIterators / AsyncGenerators,
66
+ * we use a callback for each line.
67
+ *
68
+ * @param readableStream can be a NodeJS stream or a WebAPI stream
69
+ * @param onLine will be called for every line ('\n' delimited) in the stream
70
+ */
71
+ async function readLines(
72
+ readableStream: ReadableStream<Uint8Array> | NodeJS.ReadableStream,
73
+ onLine: (line: string) => void,
74
+ textDecoder: TextDecoderEsque,
75
+ ) {
76
+ let partOfLine = '';
77
+
78
+ const onChunk = (chunk: Uint8Array) => {
79
+ const chunkText = textDecoder.decode(chunk);
80
+ const chunkLines = chunkText.split('\n');
81
+ if (chunkLines.length === 1) {
82
+ partOfLine += chunkLines[0];
83
+ } else if (chunkLines.length > 1) {
84
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length checked on line above
85
+ onLine(partOfLine + chunkLines[0]!);
86
+ for (let i = 1; i < chunkLines.length - 1; i++) {
87
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length checked on line above
88
+ onLine(chunkLines[i]!);
89
+ }
90
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length doesn't change, so is necessarily > 1
91
+ partOfLine = chunkLines[chunkLines.length - 1]!;
92
+ }
93
+ };
94
+
95
+ // we handle 2 different types of streams, this if where we figure out which one we have
96
+ if ('getReader' in readableStream) {
97
+ await readStandardChunks(readableStream, onChunk);
98
+ } else {
99
+ await readNodeChunks(readableStream, onChunk);
100
+ }
101
+
102
+ onLine(partOfLine);
103
+ }
104
+
105
+ /**
106
+ * Handle NodeJS stream
107
+ */
108
+ function readNodeChunks(
109
+ stream: NodeJS.ReadableStream,
110
+ onChunk: (chunk: Uint8Array) => void,
111
+ ) {
112
+ return new Promise<void>((resolve) => {
113
+ stream.on('data', onChunk);
114
+ stream.on('end', resolve);
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Handle WebAPI stream
120
+ */
121
+ async function readStandardChunks(
122
+ stream: ReadableStream<Uint8Array>,
123
+ onChunk: (chunk: Uint8Array) => void,
124
+ ) {
125
+ const reader = stream.getReader();
126
+ let readResult = await reader.read();
127
+ while (!readResult.done) {
128
+ onChunk(readResult.value);
129
+ readResult = await reader.read();
130
+ }
131
+ }
132
+
133
+ export const streamingJsonHttpRequester = (
134
+ opts: HTTPBaseRequestOptions & {
135
+ headers: () => HTTPHeaders | Promise<HTTPHeaders>;
136
+ textDecoder: TextDecoderEsque;
137
+ },
138
+ onSingle: (index: number, res: HTTPResult) => void,
139
+ ) => {
140
+ const ac = opts.AbortController ? new opts.AbortController() : null;
141
+ const responsePromise = fetchHTTPResponse(
142
+ {
143
+ ...opts,
144
+ contentTypeHeader: 'application/json',
145
+ batchModeHeader: 'stream',
146
+ getUrl,
147
+ getBody,
148
+ },
149
+ ac,
150
+ );
151
+ const cancel = () => ac?.abort();
152
+ const promise = responsePromise.then(async (res) => {
153
+ if (!res.body) throw new Error('Received response without body');
154
+ const meta: HTTPResult['meta'] = { response: res };
155
+ return parseJSONStream<HTTPResult>({
156
+ readableStream: res.body,
157
+ onSingle,
158
+ parse: (string) => ({
159
+ json: JSON.parse(string) as TRPCResponse,
160
+ meta,
161
+ }),
162
+ signal: ac?.signal,
163
+ textDecoder: opts.textDecoder,
164
+ });
165
+ });
166
+
167
+ return { cancel, promise };
168
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @internal
3
+ */
4
+ export interface TextDecoderEsque {
5
+ decode(chunk: Uint8Array): string;
6
+ }