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