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

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 +124 -98
  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 -94
  40. package/dist/links/httpSubscriptionLink.mjs +132 -96
  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 +54 -6
  64. package/dist/links/wsLink.d.ts.map +1 -1
  65. package/dist/links/wsLink.js +244 -175
  66. package/dist/links/wsLink.mjs +245 -176
  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 -115
  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 +308 -181
  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,23 +1,31 @@
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
- SSEMessage,
7
13
  } from '@trpc/server/unstable-core-do-not-import';
8
14
  import {
9
15
  run,
10
16
  sseStreamConsumer,
11
17
  } from '@trpc/server/unstable-core-do-not-import';
18
+ import { inputWithTrackedEventId } from '../internals/inputWithTrackedEventId';
19
+ import { raceAbortSignals } from '../internals/signals';
12
20
  import { TRPCClientError } from '../TRPCClientError';
21
+ import type { TRPCConnectionState } from '../unstable-internals';
13
22
  import { getTransformer, type TransformerOptions } from '../unstable-internals';
14
23
  import { getUrl } from './internals/httpUtils';
15
- import type { CallbackOrValue } from './internals/urlWithConnectionParams';
16
24
  import {
17
25
  resultOf,
18
26
  type UrlOptionsWithConnectionParams,
19
27
  } from './internals/urlWithConnectionParams';
20
- import type { TRPCLink } from './types';
28
+ import type { Operation, TRPCLink } from './types';
21
29
 
22
30
  async function urlWithConnectionParams(
23
31
  opts: UrlOptionsWithConnectionParams,
@@ -34,21 +42,49 @@ async function urlWithConnectionParams(
34
42
  return url;
35
43
  }
36
44
 
37
- 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;
38
53
  /**
39
54
  * EventSource options or a callback that returns them
40
55
  */
41
- eventSourceOptions?: CallbackOrValue<EventSourceInit>;
56
+ eventSourceOptions?:
57
+ | EventSourceLike.InitDictOf<TEventSource>
58
+ | ((opts: {
59
+ op: Operation;
60
+ }) =>
61
+ | EventSourceLike.InitDictOf<TEventSource>
62
+ | Promise<EventSourceLike.InitDictOf<TEventSource>>);
42
63
  } & TransformerOptions<TRoot> &
43
64
  UrlOptionsWithConnectionParams;
44
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
+
45
77
  /**
46
78
  * @see https://trpc.io/docs/client/links/httpSubscriptionLink
47
79
  */
48
80
  export function unstable_httpSubscriptionLink<
49
81
  TInferrable extends InferrableClientTypes,
82
+ TEventSource extends EventSourceLike.AnyConstructor,
50
83
  >(
51
- opts: HTTPSubscriptionLinkOptions<inferClientTypes<TInferrable>>,
84
+ opts: HTTPSubscriptionLinkOptions<
85
+ inferClientTypes<TInferrable>,
86
+ TEventSource
87
+ >,
52
88
  ): TRPCLink<TInferrable> {
53
89
  const transformer = getTransformer(opts.transformer);
54
90
 
@@ -56,97 +92,142 @@ export function unstable_httpSubscriptionLink<
56
92
  return ({ op }) => {
57
93
  return observable((observer) => {
58
94
  const { type, path, input } = op;
95
+
59
96
  /* istanbul ignore if -- @preserve */
60
97
  if (type !== 'subscription') {
61
98
  throw new Error('httpSubscriptionLink only supports subscriptions');
62
99
  }
63
100
 
64
- let eventSource: EventSource | null = null;
65
- let unsubscribed = false;
66
-
67
- run(async () => {
68
- const url = getUrl({
69
- transformer,
70
- url: await urlWithConnectionParams(opts),
71
- input,
72
- path,
73
- type,
74
- signal: null,
75
- });
76
-
77
- const eventSourceOptions = await resultOf(opts.eventSourceOptions);
78
- /* istanbul ignore if -- @preserve */
79
- if (unsubscribed) {
80
- // already unsubscribed - rare race condition
81
- return;
82
- }
83
- eventSource = new EventSource(url, eventSourceOptions);
84
- observer.next({
85
- result: {
86
- type: 'state',
87
- state: 'connecting',
88
- data: null,
89
- },
90
- });
91
-
92
- const onStarted = () => {
93
- observer.next({
94
- result: {
95
- type: 'started',
96
- },
97
- context: {
98
- eventSource,
99
- },
100
- });
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<{
107
+ id?: string;
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
+ });
101
128
 
102
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
103
- eventSource!.removeEventListener('open', onStarted);
104
- };
105
- // console.log('starting', new Date());
106
- eventSource.addEventListener('open', onStarted);
129
+ const connectionState = behaviorSubject<
130
+ TRPCConnectionState<TRPCClientError<any>>
131
+ >({
132
+ type: 'state',
133
+ state: 'connecting',
134
+ error: null,
135
+ });
107
136
 
108
- eventSource.addEventListener('open', () => {
137
+ const connectionSub = connectionState.subscribe({
138
+ next(state) {
109
139
  observer.next({
110
- result: {
111
- type: 'state',
112
- state: 'pending',
113
- },
140
+ result: state,
114
141
  });
115
- });
116
-
117
- eventSource.addEventListener('error', (event) => {
118
- // sseStreamConsumer handles this already
119
- if (eventSource?.readyState === EventSource.CLOSED) {
120
- return;
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
+ }
121
230
  }
122
-
123
- const error =
124
- globalThis.ErrorEvent && event instanceof ErrorEvent
125
- ? TRPCClientError.from(event.error)
126
- : TRPCClientError.from(new Error(`Unknown EventSource error`));
127
-
128
- observer.next({
129
- result: {
130
- type: 'state',
131
- state: 'connecting',
132
- data: error,
133
- },
134
- });
135
- });
136
-
137
- const iterable = sseStreamConsumer<Partial<SSEMessage>>({
138
- from: eventSource,
139
- deserialize: transformer.output.deserialize,
140
- });
141
-
142
- for await (const chunk of iterable) {
143
- // if the `sse({})`-helper is used, we always have an `id` field
144
- const data = 'id' in chunk ? chunk : chunk.data;
145
- observer.next({
146
- result: {
147
- data,
148
- },
149
- });
150
231
  }
151
232
 
152
233
  observer.next({
@@ -155,38 +236,14 @@ export function unstable_httpSubscriptionLink<
155
236
  },
156
237
  });
157
238
  observer.complete();
158
-
159
- observer.next({
160
- result: {
161
- type: 'state',
162
- state: 'idle',
163
- },
164
- });
165
239
  }).catch((error) => {
166
- const trpcError = TRPCClientError.from(error);
167
-
168
- observer.next({
169
- result: {
170
- type: 'state',
171
- state: 'error',
172
- data: TRPCClientError.from(trpcError),
173
- },
174
- });
175
- observer.error(trpcError);
240
+ observer.error(TRPCClientError.from(error));
176
241
  });
177
242
 
178
243
  return () => {
179
244
  observer.complete();
180
-
181
- observer.next({
182
- result: {
183
- type: 'state',
184
- state: 'idle',
185
- },
186
- });
187
-
188
- eventSource?.close();
189
- unsubscribed = true;
245
+ ac.abort();
246
+ connectionSub.unsubscribe();
190
247
  };
191
248
  });
192
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
+ }