@trpc/client 11.0.0-alpha-tmp-subscription-connection-state.489 → 11.0.0-alpha-tmp-12-06-react.667

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 (97) hide show
  1. package/dist/TRPCClientError.d.ts +1 -1
  2. package/dist/TRPCClientError.d.ts.map +1 -1
  3. package/dist/TRPCClientError.js +19 -1
  4. package/dist/TRPCClientError.mjs +19 -1
  5. package/dist/bundle-analysis.json +118 -92
  6. package/dist/createTRPCClient.d.ts +3 -2
  7. package/dist/createTRPCClient.d.ts.map +1 -1
  8. package/dist/createTRPCClient.js +1 -1
  9. package/dist/createTRPCClient.mjs +1 -1
  10. package/dist/index.js +6 -6
  11. package/dist/index.mjs +2 -2
  12. package/dist/internals/TRPCUntypedClient.d.ts +5 -4
  13. package/dist/internals/TRPCUntypedClient.d.ts.map +1 -1
  14. package/dist/internals/TRPCUntypedClient.js +42 -12
  15. package/dist/internals/TRPCUntypedClient.mjs +42 -12
  16. package/dist/internals/inputWithTrackedEventId.d.ts +2 -0
  17. package/dist/internals/inputWithTrackedEventId.d.ts.map +1 -0
  18. package/dist/internals/inputWithTrackedEventId.js +16 -0
  19. package/dist/internals/inputWithTrackedEventId.mjs +14 -0
  20. package/dist/internals/signals.d.ts +15 -0
  21. package/dist/internals/signals.d.ts.map +1 -0
  22. package/dist/internals/signals.js +47 -0
  23. package/dist/internals/signals.mjs +44 -0
  24. package/dist/internals/transformer.d.ts +2 -2
  25. package/dist/internals/types.d.ts +1 -1
  26. package/dist/internals/types.d.ts.map +1 -1
  27. package/dist/links/HTTPBatchLinkOptions.d.ts +1 -1
  28. package/dist/links/httpBatchLink.d.ts.map +1 -1
  29. package/dist/links/httpBatchLink.js +4 -3
  30. package/dist/links/httpBatchLink.mjs +5 -4
  31. package/dist/links/httpBatchStreamLink.d.ts.map +1 -1
  32. package/dist/links/httpBatchStreamLink.js +6 -4
  33. package/dist/links/httpBatchStreamLink.mjs +7 -5
  34. package/dist/links/httpLink.d.ts +2 -2
  35. package/dist/links/httpLink.js +3 -3
  36. package/dist/links/httpLink.mjs +3 -3
  37. package/dist/links/httpSubscriptionLink.d.ts +11 -6
  38. package/dist/links/httpSubscriptionLink.d.ts.map +1 -1
  39. package/dist/links/httpSubscriptionLink.js +130 -98
  40. package/dist/links/httpSubscriptionLink.mjs +132 -100
  41. package/dist/links/internals/contentTypes.d.ts +2 -2
  42. package/dist/links/internals/contentTypes.d.ts.map +1 -1
  43. package/dist/links/internals/httpUtils.d.ts +1 -8
  44. package/dist/links/internals/httpUtils.d.ts.map +1 -1
  45. package/dist/links/internals/httpUtils.js +1 -30
  46. package/dist/links/internals/httpUtils.mjs +2 -30
  47. package/dist/links/internals/subscriptions.d.ts +20 -0
  48. package/dist/links/internals/subscriptions.d.ts.map +1 -0
  49. package/dist/links/internals/urlWithConnectionParams.d.ts +2 -1
  50. package/dist/links/internals/urlWithConnectionParams.d.ts.map +1 -1
  51. package/dist/links/internals/urlWithConnectionParams.js +3 -2
  52. package/dist/links/internals/urlWithConnectionParams.mjs +3 -2
  53. package/dist/links/loggerLink.d.ts +5 -5
  54. package/dist/links/loggerLink.d.ts.map +1 -1
  55. package/dist/links/loggerLink.js +25 -21
  56. package/dist/links/loggerLink.mjs +25 -21
  57. package/dist/links/retryLink.d.ts +29 -0
  58. package/dist/links/retryLink.d.ts.map +1 -0
  59. package/dist/links/retryLink.js +65 -0
  60. package/dist/links/retryLink.mjs +63 -0
  61. package/dist/links/types.d.ts +4 -23
  62. package/dist/links/types.d.ts.map +1 -1
  63. package/dist/links/wsLink.d.ts +49 -5
  64. package/dist/links/wsLink.d.ts.map +1 -1
  65. package/dist/links/wsLink.js +210 -155
  66. package/dist/links/wsLink.mjs +211 -156
  67. package/dist/links.d.ts +1 -0
  68. package/dist/links.d.ts.map +1 -1
  69. package/dist/unstable-internals.d.ts +1 -0
  70. package/dist/unstable-internals.d.ts.map +1 -1
  71. package/package.json +14 -11
  72. package/src/TRPCClientError.ts +1 -1
  73. package/src/createTRPCClient.ts +28 -23
  74. package/src/internals/TRPCUntypedClient.ts +26 -15
  75. package/src/internals/inputWithTrackedEventId.ts +15 -0
  76. package/src/internals/signals.ts +54 -0
  77. package/src/internals/transformer.ts +2 -2
  78. package/src/internals/types.ts +1 -1
  79. package/src/links/HTTPBatchLinkOptions.ts +1 -1
  80. package/src/links/httpBatchLink.ts +3 -3
  81. package/src/links/httpBatchStreamLink.ts +7 -4
  82. package/src/links/httpLink.ts +2 -2
  83. package/src/links/httpSubscriptionLink.ts +172 -123
  84. package/src/links/internals/httpUtils.ts +1 -41
  85. package/src/links/internals/subscriptions.ts +26 -0
  86. package/src/links/internals/urlWithConnectionParams.ts +8 -2
  87. package/src/links/loggerLink.ts +21 -9
  88. package/src/links/retryLink.ts +101 -0
  89. package/src/links/types.ts +8 -46
  90. package/src/links/wsLink.ts +276 -173
  91. package/src/links.ts +1 -1
  92. package/src/unstable-internals.ts +1 -0
  93. package/dist/links/internals/retryLink.d.ts +0 -9
  94. package/dist/links/internals/retryLink.d.ts.map +0 -1
  95. package/dist/links/types.js +0 -7
  96. package/dist/links/types.mjs +0 -5
  97. package/src/links/internals/retryLink.ts +0 -53
@@ -1,6 +1,13 @@
1
- import { observable } from '@trpc/server/observable';
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
8
  import type {
3
9
  AnyClientTypes,
10
+ EventSourceLike,
4
11
  inferClientTypes,
5
12
  InferrableClientTypes,
6
13
  } from '@trpc/server/unstable-core-do-not-import';
@@ -8,15 +15,17 @@ import {
8
15
  run,
9
16
  sseStreamConsumer,
10
17
  } from '@trpc/server/unstable-core-do-not-import';
18
+ import { inputWithTrackedEventId } from '../internals/inputWithTrackedEventId';
19
+ import { raceAbortSignals } from '../internals/signals';
11
20
  import { TRPCClientError } from '../TRPCClientError';
21
+ import type { TRPCConnectionState } from '../unstable-internals';
12
22
  import { getTransformer, type TransformerOptions } from '../unstable-internals';
13
23
  import { getUrl } from './internals/httpUtils';
14
- import type { CallbackOrValue } from './internals/urlWithConnectionParams';
15
24
  import {
16
25
  resultOf,
17
26
  type UrlOptionsWithConnectionParams,
18
27
  } from './internals/urlWithConnectionParams';
19
- import type { TRPCLink } from './types';
28
+ import type { Operation, TRPCLink } from './types';
20
29
 
21
30
  async function urlWithConnectionParams(
22
31
  opts: UrlOptionsWithConnectionParams,
@@ -33,21 +42,49 @@ async function urlWithConnectionParams(
33
42
  return url;
34
43
  }
35
44
 
36
- type HTTPSubscriptionLinkOptions<TRoot extends AnyClientTypes> = {
45
+ type HTTPSubscriptionLinkOptions<
46
+ TRoot extends AnyClientTypes,
47
+ TEventSource extends EventSourceLike.AnyConstructor = typeof EventSource,
48
+ > = {
49
+ /**
50
+ * EventSource ponyfill
51
+ */
52
+ EventSource?: TEventSource;
37
53
  /**
38
54
  * EventSource options or a callback that returns them
39
55
  */
40
- eventSourceOptions?: CallbackOrValue<EventSourceInit>;
56
+ eventSourceOptions?:
57
+ | EventSourceLike.InitDictOf<TEventSource>
58
+ | ((opts: {
59
+ op: Operation;
60
+ }) =>
61
+ | EventSourceLike.InitDictOf<TEventSource>
62
+ | Promise<EventSourceLike.InitDictOf<TEventSource>>);
41
63
  } & TransformerOptions<TRoot> &
42
64
  UrlOptionsWithConnectionParams;
43
65
 
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
+
44
77
  /**
45
78
  * @see https://trpc.io/docs/client/links/httpSubscriptionLink
46
79
  */
47
80
  export function unstable_httpSubscriptionLink<
48
81
  TInferrable extends InferrableClientTypes,
82
+ TEventSource extends EventSourceLike.AnyConstructor,
49
83
  >(
50
- opts: HTTPSubscriptionLinkOptions<inferClientTypes<TInferrable>>,
84
+ opts: HTTPSubscriptionLinkOptions<
85
+ inferClientTypes<TInferrable>,
86
+ TEventSource
87
+ >,
51
88
  ): TRPCLink<TInferrable> {
52
89
  const transformer = getTransformer(opts.transformer);
53
90
 
@@ -55,106 +92,142 @@ export function unstable_httpSubscriptionLink<
55
92
  return ({ op }) => {
56
93
  return observable((observer) => {
57
94
  const { type, path, input } = op;
95
+
58
96
  /* istanbul ignore if -- @preserve */
59
97
  if (type !== 'subscription') {
60
98
  throw new Error('httpSubscriptionLink only supports subscriptions');
61
99
  }
62
100
 
63
- let eventSource: EventSource | null = null;
64
- let unsubscribed = false;
65
-
66
- run(async () => {
67
- const url = getUrl({
68
- transformer,
69
- url: await urlWithConnectionParams(opts),
70
- input,
71
- path,
72
- type,
73
- signal: null,
74
- });
75
-
76
- const eventSourceOptions = await resultOf(opts.eventSourceOptions);
77
- /* istanbul ignore if -- @preserve */
78
- if (unsubscribed) {
79
- // already unsubscribed - rare race condition
80
- return;
81
- }
82
- eventSource = new EventSource(url, eventSourceOptions);
83
- observer.next({
84
- result: {
85
- type: 'state',
86
- state: 'connecting',
87
- data: null,
88
- },
89
- });
90
-
91
- const onStarted = () => {
92
- observer.next({
93
- result: {
94
- type: 'started',
95
- },
96
- context: {
97
- eventSource,
98
- },
99
- });
100
-
101
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
102
- eventSource!.removeEventListener('open', onStarted);
103
- };
104
- // console.log('starting', new Date());
105
- eventSource.addEventListener('open', onStarted);
106
-
107
- eventSource.addEventListener('open', () => {
108
- observer.next({
109
- result: {
110
- type: 'state',
111
- state: 'pending',
112
- },
113
- });
114
- });
115
-
116
- eventSource.addEventListener('error', (event) => {
117
- // sseStreamConsumer handles this already
118
- if (eventSource?.readyState === EventSource.CLOSED) {
119
- return;
120
- }
121
-
122
- const error =
123
- globalThis.ErrorEvent && event instanceof ErrorEvent
124
- ? TRPCClientError.from(event.error)
125
- : TRPCClientError.from(new Error(`Unknown EventSource error`));
126
-
127
- observer.next({
128
- result: {
129
- type: 'state',
130
- state: 'connecting',
131
- data: error,
132
- },
133
- });
134
- });
135
-
136
- const iterable = sseStreamConsumer<{
101
+ let lastEventId: string | undefined = undefined;
102
+ const ac = new AbortController();
103
+ const signal = raceAbortSignals(op.signal, ac.signal);
104
+ const eventSourceStream = sseStreamConsumer<{
105
+ EventSource: TEventSource;
106
+ data: Partial<{
137
107
  id?: string;
138
- data?: unknown;
139
- }>({
140
- from: eventSource,
141
- deserialize: transformer.output.deserialize,
142
- });
108
+ data: unknown;
109
+ }>;
110
+ error: TRPCErrorShape;
111
+ }>({
112
+ url: async () =>
113
+ getUrl({
114
+ transformer,
115
+ url: await urlWithConnectionParams(opts),
116
+ input: inputWithTrackedEventId(input, lastEventId),
117
+ path,
118
+ type,
119
+ signal: null,
120
+ }),
121
+ init: () => resultOf(opts.eventSourceOptions, { op }),
122
+ signal,
123
+ deserialize: transformer.output.deserialize,
124
+ EventSource:
125
+ opts.EventSource ??
126
+ (globalThis.EventSource as never as TEventSource),
127
+ });
143
128
 
144
- for await (const chunk of iterable) {
145
- if (!chunk.ok) {
146
- // TODO: handle in https://github.com/trpc/trpc/issues/5871
147
- continue;
148
- }
149
- const chunkData = chunk.data;
129
+ const connectionState = behaviorSubject<
130
+ TRPCConnectionState<TRPCClientError<any>>
131
+ >({
132
+ type: 'state',
133
+ state: 'connecting',
134
+ error: null,
135
+ });
150
136
 
151
- // if the `tracked()`-helper is used, we always have an `id` field
152
- const data = 'id' in chunkData ? chunkData : chunkData.data;
137
+ const connectionSub = connectionState.subscribe({
138
+ next(state) {
153
139
  observer.next({
154
- result: {
155
- data,
156
- },
140
+ result: state,
157
141
  });
142
+ },
143
+ });
144
+ run(async () => {
145
+ for await (const chunk of eventSourceStream) {
146
+ switch (chunk.type) {
147
+ case 'ping':
148
+ // do nothing
149
+ break;
150
+ case 'data':
151
+ const chunkData = chunk.data;
152
+
153
+ let result: TRPCResult<unknown>;
154
+ if (chunkData.id) {
155
+ // if the `tracked()`-helper is used, we always have an `id` field
156
+ lastEventId = chunkData.id;
157
+ result = {
158
+ id: chunkData.id,
159
+ data: chunkData,
160
+ };
161
+ } else {
162
+ result = {
163
+ data: chunkData.data,
164
+ };
165
+ }
166
+
167
+ observer.next({
168
+ result,
169
+ context: {
170
+ eventSource: chunk.eventSource,
171
+ },
172
+ });
173
+ break;
174
+ case 'connected': {
175
+ observer.next({
176
+ result: {
177
+ type: 'started',
178
+ },
179
+ context: {
180
+ eventSource: chunk.eventSource,
181
+ },
182
+ });
183
+ connectionState.next({
184
+ type: 'state',
185
+ state: 'pending',
186
+ error: null,
187
+ });
188
+ break;
189
+ }
190
+ case 'serialized-error': {
191
+ const error = TRPCClientError.from({ error: chunk.error });
192
+
193
+ if (codes5xx.includes(chunk.error.code)) {
194
+ //
195
+ connectionState.next({
196
+ type: 'state',
197
+ state: 'connecting',
198
+ error,
199
+ });
200
+ break;
201
+ }
202
+ //
203
+ // non-retryable error, cancel the subscription
204
+ throw error;
205
+ }
206
+ case 'connecting': {
207
+ const lastState = connectionState.get();
208
+
209
+ const error = chunk.event && TRPCClientError.from(chunk.event);
210
+ if (!error && lastState.state === 'connecting') {
211
+ break;
212
+ }
213
+
214
+ connectionState.next({
215
+ type: 'state',
216
+ state: 'connecting',
217
+ error,
218
+ });
219
+ break;
220
+ }
221
+ case 'timeout': {
222
+ connectionState.next({
223
+ type: 'state',
224
+ state: 'connecting',
225
+ error: new TRPCClientError(
226
+ `Timeout of ${chunk.ms}ms reached while waiting for a response`,
227
+ ),
228
+ });
229
+ }
230
+ }
158
231
  }
159
232
 
160
233
  observer.next({
@@ -163,38 +236,14 @@ export function unstable_httpSubscriptionLink<
163
236
  },
164
237
  });
165
238
  observer.complete();
166
-
167
- observer.next({
168
- result: {
169
- type: 'state',
170
- state: 'idle',
171
- },
172
- });
173
239
  }).catch((error) => {
174
- const trpcError = TRPCClientError.from(error);
175
-
176
- observer.next({
177
- result: {
178
- type: 'state',
179
- state: 'error',
180
- data: TRPCClientError.from(trpcError),
181
- },
182
- });
183
- observer.error(trpcError);
240
+ observer.error(TRPCClientError.from(error));
184
241
  });
185
242
 
186
243
  return () => {
187
244
  observer.complete();
188
-
189
- observer.next({
190
- result: {
191
- type: 'state',
192
- state: 'idle',
193
- },
194
- });
195
-
196
- eventSource?.close();
197
- unsubscribed = true;
245
+ ac.abort();
246
+ connectionSub.unsubscribe();
198
247
  };
199
248
  });
200
249
  };
@@ -30,7 +30,7 @@ export type HTTPLinkBaseOptions<
30
30
  /**
31
31
  * Send all requests `as POST`s requests regardless of the procedure type
32
32
  * The HTTP handler must separately allow overriding the method. See:
33
- * @link https://trpc.io/docs/rpc
33
+ * @see https://trpc.io/docs/rpc
34
34
  */
35
35
  methodOverride?: 'POST';
36
36
  } & TransformerOptions<TRoot>;
@@ -240,43 +240,3 @@ export async function httpRequest(
240
240
  meta,
241
241
  };
242
242
  }
243
-
244
- /**
245
- * Merges multiple abort signals into a single one
246
- * - When all signals have been aborted, the merged signal will be aborted
247
- */
248
- export function mergeAbortSignals(
249
- opts: {
250
- signal: Maybe<AbortSignal>;
251
- }[],
252
- ): AbortController {
253
- const ac = new AbortController();
254
-
255
- if (opts.some((o) => !o.signal)) {
256
- return ac;
257
- }
258
-
259
- const count = opts.length;
260
-
261
- let abortedCount = 0;
262
-
263
- const onAbort = () => {
264
- if (++abortedCount === count) {
265
- ac.abort();
266
- }
267
- };
268
-
269
- for (const o of opts) {
270
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
271
- const signal = o.signal!;
272
- if (signal.aborted) {
273
- onAbort();
274
- } else {
275
- signal.addEventListener('abort', onAbort, {
276
- once: true,
277
- });
278
- }
279
- }
280
-
281
- return ac;
282
- }
@@ -0,0 +1,26 @@
1
+ interface ConnectionStateBase<TError> {
2
+ type: 'state';
3
+ data?: never;
4
+ error: TError | null;
5
+ }
6
+
7
+ interface ConnectionIdleState<TError> extends ConnectionStateBase<TError> {
8
+ state: 'idle';
9
+ error: null;
10
+ }
11
+
12
+ interface ConnectionConnectingState<TError>
13
+ extends ConnectionStateBase<TError> {
14
+ state: 'connecting';
15
+ error: TError | null;
16
+ }
17
+
18
+ interface ConnectionPendingState extends ConnectionStateBase<never> {
19
+ state: 'pending';
20
+ error: null;
21
+ }
22
+
23
+ export type TRPCConnectionState<TError> =
24
+ | ConnectionIdleState<TError>
25
+ | ConnectionConnectingState<TError>
26
+ | ConnectionPendingState;
@@ -2,9 +2,15 @@ import { type TRPCRequestInfo } from '@trpc/server/http';
2
2
 
3
3
  /**
4
4
  * Get the result of a value or function that returns a value
5
+ * It also optionally accepts typesafe arguments for the function
5
6
  */
6
- export const resultOf = <T>(value: T | (() => T)): T => {
7
- return typeof value === 'function' ? (value as () => T)() : value;
7
+ export const resultOf = <T, TArgs extends any[]>(
8
+ value: T | ((...args: TArgs) => T),
9
+ ...args: TArgs
10
+ ): T => {
11
+ return typeof value === 'function'
12
+ ? (value as (...args: TArgs) => T)(...args)
13
+ : value;
8
14
  };
9
15
 
10
16
  /**
@@ -6,7 +6,10 @@
6
6
  // even if end-user `tsconfig.json` omits it in the `lib` array.
7
7
 
8
8
  import { observable, tap } from '@trpc/server/observable';
9
- import type { AnyRouter } from '@trpc/server/unstable-core-do-not-import';
9
+ import type {
10
+ AnyRouter,
11
+ InferrableClientTypes,
12
+ } from '@trpc/server/unstable-core-do-not-import';
10
13
  import type { TRPCClientError } from '../TRPCClientError';
11
14
  import type { Operation, OperationResultEnvelope, TRPCLink } from './types';
12
15
 
@@ -15,10 +18,12 @@ type ConsoleEsque = {
15
18
  error: (...args: any[]) => void;
16
19
  };
17
20
 
18
- type EnableFnOptions<TRouter extends AnyRouter> =
21
+ type EnableFnOptions<TRouter extends InferrableClientTypes> =
19
22
  | {
20
23
  direction: 'down';
21
- result: OperationResultEnvelope<unknown> | TRPCClientError<TRouter>;
24
+ result:
25
+ | OperationResultEnvelope<unknown, TRPCClientError<TRouter>>
26
+ | TRPCClientError<TRouter>;
22
27
  }
23
28
  | (Operation & {
24
29
  direction: 'up';
@@ -34,7 +39,9 @@ type LoggerLinkFnOptions<TRouter extends AnyRouter> = Operation &
34
39
  * Request result
35
40
  */
36
41
  direction: 'down';
37
- result: OperationResultEnvelope<unknown> | TRPCClientError<TRouter>;
42
+ result:
43
+ | OperationResultEnvelope<unknown, TRPCClientError<TRouter>>
44
+ | TRPCClientError<TRouter>;
38
45
  elapsedMs: number;
39
46
  }
40
47
  | {
@@ -193,7 +200,8 @@ const defaultLogger =
193
200
  const fn: 'error' | 'log' =
194
201
  props.direction === 'down' &&
195
202
  props.result &&
196
- (props.result instanceof Error || 'error' in props.result.result)
203
+ (props.result instanceof Error ||
204
+ ('error' in props.result.result && props.result.result.error))
197
205
  ? 'error'
198
206
  : 'log';
199
207
 
@@ -201,7 +209,7 @@ const defaultLogger =
201
209
  };
202
210
 
203
211
  /**
204
- * @link https://trpc.io/docs/v11/client/links/loggerLink
212
+ * @see https://trpc.io/docs/v11/client/links/loggerLink
205
213
  */
206
214
  export function loggerLink<TRouter extends AnyRouter = AnyRouter>(
207
215
  opts: LoggerLinkOptions<TRouter> = {},
@@ -219,24 +227,28 @@ export function loggerLink<TRouter extends AnyRouter = AnyRouter>(
219
227
  return ({ op, next }) => {
220
228
  return observable((observer) => {
221
229
  // ->
222
- enabled({ ...op, direction: 'up' }) &&
230
+ if (enabled({ ...op, direction: 'up' })) {
223
231
  logger({
224
232
  ...op,
225
233
  direction: 'up',
226
234
  });
235
+ }
227
236
  const requestStartTime = Date.now();
228
237
  function logResult(
229
- result: OperationResultEnvelope<unknown> | TRPCClientError<TRouter>,
238
+ result:
239
+ | OperationResultEnvelope<unknown, TRPCClientError<TRouter>>
240
+ | TRPCClientError<TRouter>,
230
241
  ) {
231
242
  const elapsedMs = Date.now() - requestStartTime;
232
243
 
233
- enabled({ ...op, direction: 'down', result }) &&
244
+ if (enabled({ ...op, direction: 'down', result })) {
234
245
  logger({
235
246
  ...op,
236
247
  direction: 'down',
237
248
  elapsedMs,
238
249
  result,
239
250
  });
251
+ }
240
252
  }
241
253
  return next(op)
242
254
  .pipe(
@@ -0,0 +1,101 @@
1
+ /* istanbul ignore file -- @preserve */
2
+ // We're not actually exporting this link
3
+ import type { Unsubscribable } from '@trpc/server/observable';
4
+ import { observable } from '@trpc/server/observable';
5
+ import type { InferrableClientTypes } from '@trpc/server/unstable-core-do-not-import';
6
+ import { inputWithTrackedEventId } from '../internals/inputWithTrackedEventId';
7
+ import type { TRPCClientError } from '../TRPCClientError';
8
+ import type { Operation, TRPCLink } from './types';
9
+
10
+ interface RetryLinkOptions<TInferrable extends InferrableClientTypes> {
11
+ /**
12
+ * The retry function
13
+ */
14
+ retry: (opts: RetryFnOptions<TInferrable>) => boolean;
15
+ }
16
+
17
+ interface RetryFnOptions<TInferrable extends InferrableClientTypes> {
18
+ /**
19
+ * The operation that failed
20
+ */
21
+ op: Operation;
22
+ /**
23
+ * The error that occurred
24
+ */
25
+ error: TRPCClientError<TInferrable>;
26
+ /**
27
+ * The number of attempts that have been made (including the first call)
28
+ */
29
+ attempts: number;
30
+ }
31
+
32
+ /**
33
+ * @see https://trpc.io/docs/v11/client/links/retryLink
34
+ */
35
+ export function retryLink<TInferrable extends InferrableClientTypes>(
36
+ opts: RetryLinkOptions<TInferrable>,
37
+ ): TRPCLink<TInferrable> {
38
+ // initialized config
39
+ return () => {
40
+ // initialized in app
41
+ return (callOpts) => {
42
+ // initialized for request
43
+ return observable((observer) => {
44
+ let next$: Unsubscribable;
45
+
46
+ let lastEventId: string | undefined = undefined;
47
+
48
+ attempt(1);
49
+
50
+ function opWithLastEventId() {
51
+ const op = callOpts.op;
52
+ if (!lastEventId) {
53
+ return op;
54
+ }
55
+
56
+ return {
57
+ ...op,
58
+ input: inputWithTrackedEventId(op.input, lastEventId),
59
+ };
60
+ }
61
+
62
+ function attempt(attempts: number) {
63
+ const op = opWithLastEventId();
64
+
65
+ next$ = callOpts.next(op).subscribe({
66
+ error(error) {
67
+ const shouldRetry = opts.retry({
68
+ op,
69
+ attempts,
70
+ error,
71
+ });
72
+ if (shouldRetry) {
73
+ attempt(attempts + 1);
74
+ } else {
75
+ observer.error(error);
76
+ }
77
+ },
78
+ next(envelope) {
79
+ //
80
+ if (
81
+ (!envelope.result.type || envelope.result.type === 'data') &&
82
+ envelope.result.id
83
+ ) {
84
+ //
85
+ lastEventId = envelope.result.id;
86
+ }
87
+
88
+ observer.next(envelope);
89
+ },
90
+ complete() {
91
+ observer.complete();
92
+ },
93
+ });
94
+ }
95
+ return () => {
96
+ next$.unsubscribe();
97
+ };
98
+ });
99
+ };
100
+ };
101
+ }