@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.
Files changed (40) 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 +147 -2
  8. package/dist/index.mjs +149 -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/httpBatchLink.d.ts +1 -15
  16. package/dist/links/httpBatchLink.d.ts.map +1 -1
  17. package/dist/links/httpBatchLink.js +5 -217
  18. package/dist/links/httpBatchLink.mjs +4 -220
  19. package/dist/links/httpBatchStreamLink.d.ts +2 -0
  20. package/dist/links/httpBatchStreamLink.d.ts.map +1 -0
  21. package/dist/links/httpLink.js +1 -1
  22. package/dist/links/httpLink.mjs +1 -1
  23. package/dist/links/index.d.ts +2 -0
  24. package/dist/links/index.d.ts.map +1 -1
  25. package/dist/links/internals/createHTTPBatchLink.d.ts +20 -0
  26. package/dist/links/internals/createHTTPBatchLink.d.ts.map +1 -0
  27. package/dist/links/internals/httpUtils.d.ts +3 -1
  28. package/dist/links/internals/httpUtils.d.ts.map +1 -1
  29. package/dist/links/internals/parseJSONStream.d.ts +36 -0
  30. package/dist/links/internals/parseJSONStream.d.ts.map +1 -0
  31. package/package.json +4 -4
  32. package/src/internals/dataLoader.ts +21 -7
  33. package/src/internals/types.ts +1 -0
  34. package/src/links/HTTPBatchLinkOptions.ts +21 -0
  35. package/src/links/httpBatchLink.ts +44 -133
  36. package/src/links/httpBatchStreamLink.ts +47 -0
  37. package/src/links/index.ts +2 -0
  38. package/src/links/internals/createHTTPBatchLink.ts +113 -0
  39. package/src/links/internals/httpUtils.ts +33 -24
  40. 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
- 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;
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
- 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
- });
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);
@@ -1,6 +1,8 @@
1
1
  export * from './types';
2
2
 
3
3
  export * from './httpBatchLink';
4
+ export * from './httpBatchStreamLink';
5
+ export * from './HTTPBatchLinkOptions';
4
6
  export * from './httpLink';
5
7
  export * from './loggerLink';
6
8
  export * from './splitLink';
@@ -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
- 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
- })
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
+ };