@trpc/client 11.2.1-canary.0 → 11.2.1-canary.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trpc/client",
3
- "version": "11.2.1-canary.0+836788f4f",
3
+ "version": "11.2.1-canary.3+46afe6590",
4
4
  "description": "The tRPC client library",
5
5
  "author": "KATT",
6
6
  "license": "MIT",
@@ -77,11 +77,11 @@
77
77
  "!**/__tests__"
78
78
  ],
79
79
  "peerDependencies": {
80
- "@trpc/server": "11.2.1-canary.0+836788f4f",
80
+ "@trpc/server": "11.2.1-canary.3+46afe6590",
81
81
  "typescript": ">=5.7.2"
82
82
  },
83
83
  "devDependencies": {
84
- "@trpc/server": "11.2.1-canary.0+836788f4f",
84
+ "@trpc/server": "11.2.1-canary.3+46afe6590",
85
85
  "@types/isomorphic-fetch": "^0.0.39",
86
86
  "@types/node": "^22.13.5",
87
87
  "dataloader": "^2.2.2",
@@ -101,5 +101,5 @@
101
101
  "funding": [
102
102
  "https://trpc.io/sponsor"
103
103
  ],
104
- "gitHead": "836788f4fea787be31bbec1ab2e692fa38297ad0"
104
+ "gitHead": "46afe65907644bafaf7e60910db41a58dfa350fb"
105
105
  }
@@ -19,15 +19,10 @@ export interface TRPCClientErrorBase<TShape extends DefaultErrorShape> {
19
19
  export type TRPCClientErrorLike<TInferrable extends InferrableClientTypes> =
20
20
  TRPCClientErrorBase<inferErrorShape<TInferrable>>;
21
21
 
22
- function isTRPCClientError(cause: unknown): cause is TRPCClientError<any> {
23
- return (
24
- cause instanceof TRPCClientError ||
25
- /**
26
- * @deprecated
27
- * Delete in next major
28
- */
29
- (cause instanceof Error && cause.name === 'TRPCClientError')
30
- );
22
+ export function isTRPCClientError<TInferrable extends InferrableClientTypes>(
23
+ cause: unknown,
24
+ ): cause is TRPCClientError<TInferrable> {
25
+ return cause instanceof TRPCClientError;
31
26
  }
32
27
 
33
28
  function isTRPCErrorResponse(obj: unknown): obj is TRPCErrorResponse<any> {
@@ -52,3 +52,19 @@ export function raceAbortSignals(
52
52
 
53
53
  return ac.signal;
54
54
  }
55
+
56
+ export function abortSignalToPromise(signal: AbortSignal): Promise<never> {
57
+ return new Promise((_, reject) => {
58
+ if (signal.aborted) {
59
+ reject(signal.reason);
60
+ return;
61
+ }
62
+ signal.addEventListener(
63
+ 'abort',
64
+ () => {
65
+ reject(signal.reason);
66
+ },
67
+ { once: true },
68
+ );
69
+ });
70
+ }
@@ -1,10 +1,5 @@
1
1
  import { behaviorSubject, observable } from '@trpc/server/observable';
2
- import type {
3
- TRPC_ERROR_CODE_NUMBER,
4
- TRPCErrorShape,
5
- TRPCResult,
6
- } from '@trpc/server/rpc';
7
- import { TRPC_ERROR_CODES_BY_KEY } from '@trpc/server/rpc';
2
+ import type { TRPCErrorShape, TRPCResult } from '@trpc/server/rpc';
8
3
  import type {
9
4
  AnyClientTypes,
10
5
  EventSourceLike,
@@ -12,6 +7,7 @@ import type {
12
7
  InferrableClientTypes,
13
8
  } from '@trpc/server/unstable-core-do-not-import';
14
9
  import {
10
+ retryableRpcCodes,
15
11
  run,
16
12
  sseStreamConsumer,
17
13
  } from '@trpc/server/unstable-core-do-not-import';
@@ -63,17 +59,6 @@ type HTTPSubscriptionLinkOptions<
63
59
  } & TransformerOptions<TRoot> &
64
60
  UrlOptionsWithConnectionParams;
65
61
 
66
- /**
67
- * tRPC error codes that are considered retryable
68
- * With out of the box SSE, the client will reconnect when these errors are encountered
69
- */
70
- const codes5xx: TRPC_ERROR_CODE_NUMBER[] = [
71
- TRPC_ERROR_CODES_BY_KEY.BAD_GATEWAY,
72
- TRPC_ERROR_CODES_BY_KEY.SERVICE_UNAVAILABLE,
73
- TRPC_ERROR_CODES_BY_KEY.GATEWAY_TIMEOUT,
74
- TRPC_ERROR_CODES_BY_KEY.INTERNAL_SERVER_ERROR,
75
- ];
76
-
77
62
  /**
78
63
  * @see https://trpc.io/docs/client/links/httpSubscriptionLink
79
64
  */
@@ -190,7 +175,7 @@ export function httpSubscriptionLink<
190
175
  case 'serialized-error': {
191
176
  const error = TRPCClientError.from({ error: chunk.error });
192
177
 
193
- if (codes5xx.includes(chunk.error.code)) {
178
+ if (retryableRpcCodes.includes(chunk.error.code)) {
194
179
  //
195
180
  connectionState.next({
196
181
  type: 'state',
@@ -0,0 +1,277 @@
1
+ import {
2
+ getTRPCErrorFromUnknown,
3
+ getTRPCErrorShape,
4
+ isTrackedEnvelope,
5
+ } from '@trpc/server';
6
+ import { behaviorSubject, observable } from '@trpc/server/observable';
7
+ import { TRPC_ERROR_CODES_BY_KEY, type TRPCResult } from '@trpc/server/rpc';
8
+ import {
9
+ callProcedure,
10
+ isAbortError,
11
+ isAsyncIterable,
12
+ iteratorResource,
13
+ makeResource,
14
+ retryableRpcCodes,
15
+ run,
16
+ type AnyRouter,
17
+ type ErrorHandlerOptions,
18
+ type inferClientTypes,
19
+ type inferRouterContext,
20
+ } from '@trpc/server/unstable-core-do-not-import';
21
+ import { inputWithTrackedEventId } from '../internals/inputWithTrackedEventId';
22
+ import { abortSignalToPromise, raceAbortSignals } from '../internals/signals';
23
+ import { getTransformer } from '../internals/transformer';
24
+ import type { TransformerOptions } from '../internals/transformer';
25
+ import { isTRPCClientError, TRPCClientError } from '../TRPCClientError';
26
+ import type { TRPCConnectionState } from './internals/subscriptions';
27
+ import type { TRPCLink } from './types';
28
+
29
+ export type LocalLinkOptions<TRouter extends AnyRouter> = {
30
+ router: TRouter;
31
+ createContext: () => Promise<inferRouterContext<TRouter>>;
32
+ onError?: (opts: ErrorHandlerOptions<inferRouterContext<TRouter>>) => void;
33
+ } & TransformerOptions<inferClientTypes<TRouter>>;
34
+
35
+ /**
36
+ * localLink is a terminating link that allows you to make tRPC procedure calls directly in your application without going through HTTP.
37
+ *
38
+ * @see https://trpc.io/docs/links/localLink
39
+ */
40
+ export function experimental_localLink<TRouter extends AnyRouter>(
41
+ opts: LocalLinkOptions<TRouter>,
42
+ ): TRPCLink<TRouter> {
43
+ const transformer = getTransformer(opts.transformer);
44
+
45
+ const transformChunk = (chunk: unknown) => {
46
+ if (opts.transformer) {
47
+ // assume transformer will do the right thing
48
+ return chunk;
49
+ }
50
+ // Special case for undefined, because `JSON.stringify(undefined)` throws
51
+ if (chunk === undefined) {
52
+ return chunk;
53
+ }
54
+ const serialized = JSON.stringify(transformer.input.serialize(chunk));
55
+ const deserialized = JSON.parse(transformer.output.deserialize(serialized));
56
+ return deserialized;
57
+ };
58
+
59
+ return () =>
60
+ ({ op }) =>
61
+ observable((observer) => {
62
+ let ctx: inferRouterContext<TRouter> | undefined = undefined;
63
+ const ac = new AbortController();
64
+
65
+ const signal = raceAbortSignals(op.signal, ac.signal);
66
+ const signalPromise = abortSignalToPromise(signal);
67
+
68
+ signalPromise.catch(() => {
69
+ // prevent unhandled rejection
70
+ });
71
+
72
+ let input = op.input;
73
+ async function runProcedure(newInput: unknown): Promise<unknown> {
74
+ input = newInput;
75
+
76
+ ctx = await opts.createContext();
77
+
78
+ return callProcedure({
79
+ router: opts.router,
80
+ path: op.path,
81
+ getRawInput: async () => newInput,
82
+ ctx,
83
+ type: op.type,
84
+ signal,
85
+ });
86
+ }
87
+
88
+ function onErrorCallback(cause: unknown) {
89
+ if (isAbortError(cause)) {
90
+ return;
91
+ }
92
+ opts.onError?.({
93
+ error: getTRPCErrorFromUnknown(cause),
94
+ type: op.type,
95
+ path: op.path,
96
+ input,
97
+ ctx,
98
+ });
99
+ }
100
+
101
+ function coerceToTRPCClientError(cause: unknown) {
102
+ if (isTRPCClientError<TRouter>(cause)) {
103
+ return cause;
104
+ }
105
+ const error = getTRPCErrorFromUnknown(cause);
106
+
107
+ const shape = getTRPCErrorShape({
108
+ config: opts.router._def._config,
109
+ ctx,
110
+ error,
111
+ input,
112
+ path: op.path,
113
+ type: op.type,
114
+ });
115
+ return TRPCClientError.from({
116
+ error: transformChunk(shape),
117
+ });
118
+ }
119
+
120
+ run(async () => {
121
+ switch (op.type) {
122
+ case 'query':
123
+ case 'mutation': {
124
+ const result = await runProcedure(op.input);
125
+ if (!isAsyncIterable(result)) {
126
+ observer.next({
127
+ result: { data: transformChunk(result) },
128
+ });
129
+ observer.complete();
130
+ break;
131
+ }
132
+
133
+ observer.next({
134
+ result: {
135
+ data: (async function* () {
136
+ await using iterator = iteratorResource(result);
137
+ using _finally = makeResource({}, () => {
138
+ observer.complete();
139
+ });
140
+ try {
141
+ while (true) {
142
+ const res = await Promise.race([
143
+ iterator.next(),
144
+ signalPromise,
145
+ ]);
146
+ if (res.done) {
147
+ return transformChunk(res.value);
148
+ }
149
+ yield transformChunk(res.value);
150
+ }
151
+ } catch (cause) {
152
+ onErrorCallback(cause);
153
+ throw coerceToTRPCClientError(cause);
154
+ }
155
+ })(),
156
+ },
157
+ });
158
+ break;
159
+ }
160
+ case 'subscription': {
161
+ const connectionState = behaviorSubject<
162
+ TRPCConnectionState<TRPCClientError<any>>
163
+ >({
164
+ type: 'state',
165
+ state: 'connecting',
166
+ error: null,
167
+ });
168
+
169
+ const connectionSub = connectionState.subscribe({
170
+ next(state) {
171
+ observer.next({
172
+ result: state,
173
+ });
174
+ },
175
+ });
176
+ let lastEventId: string | undefined = undefined;
177
+
178
+ using _finally = makeResource({}, async () => {
179
+ observer.complete();
180
+
181
+ connectionState.next({
182
+ type: 'state',
183
+ state: 'idle',
184
+ error: null,
185
+ });
186
+ connectionSub.unsubscribe();
187
+ });
188
+ while (true) {
189
+ const result = await runProcedure(
190
+ inputWithTrackedEventId(op.input, lastEventId),
191
+ );
192
+ if (!isAsyncIterable(result)) {
193
+ throw new Error('Expected an async iterable');
194
+ }
195
+ await using iterator = iteratorResource(result);
196
+
197
+ observer.next({
198
+ result: {
199
+ type: 'started',
200
+ },
201
+ });
202
+ connectionState.next({
203
+ type: 'state',
204
+ state: 'pending',
205
+ error: null,
206
+ });
207
+
208
+ // Use a while loop to handle errors and reconnects
209
+ while (true) {
210
+ let res;
211
+ try {
212
+ res = await Promise.race([iterator.next(), signalPromise]);
213
+ } catch (cause) {
214
+ if (isAbortError(cause)) {
215
+ return;
216
+ }
217
+ const error = getTRPCErrorFromUnknown(cause);
218
+
219
+ if (
220
+ !retryableRpcCodes.includes(
221
+ TRPC_ERROR_CODES_BY_KEY[error.code],
222
+ )
223
+ ) {
224
+ throw coerceToTRPCClientError(error);
225
+ }
226
+
227
+ onErrorCallback(error);
228
+ connectionState.next({
229
+ type: 'state',
230
+ state: 'connecting',
231
+ error: coerceToTRPCClientError(error),
232
+ });
233
+
234
+ break;
235
+ }
236
+
237
+ if (res.done) {
238
+ return;
239
+ }
240
+ let chunk: TRPCResult<unknown>;
241
+ if (isTrackedEnvelope(res.value)) {
242
+ lastEventId = res.value[0];
243
+
244
+ chunk = {
245
+ id: res.value[0],
246
+ data: {
247
+ id: res.value[0],
248
+ data: res.value[1],
249
+ },
250
+ };
251
+ } else {
252
+ chunk = {
253
+ data: res.value,
254
+ };
255
+ }
256
+
257
+ observer.next({
258
+ result: {
259
+ ...chunk,
260
+ data: transformChunk(chunk.data),
261
+ },
262
+ });
263
+ }
264
+ }
265
+ break;
266
+ }
267
+ }
268
+ }).catch((cause) => {
269
+ onErrorCallback(cause);
270
+ observer.error(coerceToTRPCClientError(cause));
271
+ });
272
+
273
+ return () => {
274
+ ac.abort();
275
+ };
276
+ });
277
+ }
package/src/links.ts CHANGED
@@ -9,6 +9,7 @@ export * from './links/splitLink';
9
9
  export * from './links/wsLink/wsLink';
10
10
  export * from './links/httpSubscriptionLink';
11
11
  export * from './links/retryLink';
12
+ export * from './links/localLink';
12
13
 
13
14
  // These are not public (yet) as we get this functionality from tanstack query
14
15
  // export * from './links/internals/dedupeLink';