@trpc/client 11.0.0-rc.592 → 11.0.0-rc.593
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/TRPCClientError.d.ts +1 -1
- package/dist/TRPCClientError.d.ts.map +1 -1
- package/dist/bundle-analysis.json +58 -46
- package/dist/index.js +2 -0
- package/dist/index.mjs +1 -0
- package/dist/internals/TRPCUntypedClient.d.ts +3 -3
- package/dist/internals/TRPCUntypedClient.d.ts.map +1 -1
- package/dist/internals/TRPCUntypedClient.js +24 -8
- package/dist/internals/TRPCUntypedClient.mjs +24 -8
- package/dist/links/httpSubscriptionLink.d.ts +7 -7
- package/dist/links/httpSubscriptionLink.d.ts.map +1 -1
- package/dist/links/httpSubscriptionLink.js +61 -1
- package/dist/links/httpSubscriptionLink.mjs +62 -2
- package/dist/links/internals/retryLink.d.ts +26 -6
- package/dist/links/internals/retryLink.d.ts.map +1 -1
- package/dist/links/internals/retryLink.js +43 -0
- package/dist/links/internals/retryLink.mjs +41 -0
- package/dist/links/internals/subscriptions.d.ts +20 -0
- package/dist/links/internals/subscriptions.d.ts.map +1 -0
- package/dist/links/loggerLink.d.ts +4 -4
- package/dist/links/loggerLink.d.ts.map +1 -1
- package/dist/links/loggerLink.js +1 -1
- package/dist/links/loggerLink.mjs +1 -1
- package/dist/links/types.d.ts +5 -4
- package/dist/links/types.d.ts.map +1 -1
- package/dist/links/wsLink.d.ts +24 -1
- package/dist/links/wsLink.d.ts.map +1 -1
- package/dist/links/wsLink.js +125 -54
- package/dist/links/wsLink.mjs +126 -55
- package/dist/links.d.ts +1 -0
- package/dist/links.d.ts.map +1 -1
- package/dist/unstable-internals.d.ts +1 -0
- package/dist/unstable-internals.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/TRPCClientError.ts +1 -1
- package/src/internals/TRPCUntypedClient.ts +23 -10
- package/src/links/httpSubscriptionLink.ts +88 -17
- package/src/links/internals/retryLink.ts +42 -24
- package/src/links/internals/subscriptions.ts +26 -0
- package/src/links/loggerLink.ts +16 -6
- package/src/links/types.ts +12 -4
- package/src/links/wsLink.ts +163 -56
- package/src/links.ts +1 -1
- package/src/unstable-internals.ts +1 -0
|
@@ -2,50 +2,68 @@
|
|
|
2
2
|
// We're not actually exporting this link
|
|
3
3
|
import type { Unsubscribable } from '@trpc/server/observable';
|
|
4
4
|
import { observable } from '@trpc/server/observable';
|
|
5
|
-
import type {
|
|
6
|
-
import type {
|
|
5
|
+
import type { InferrableClientTypes } from '@trpc/server/unstable-core-do-not-import';
|
|
6
|
+
import type { TRPCClientError } from '../../TRPCClientError';
|
|
7
|
+
import type { Operation, TRPCLink } from '../types';
|
|
8
|
+
|
|
9
|
+
interface RetryLinkOptions<TInferrable extends InferrableClientTypes> {
|
|
10
|
+
/**
|
|
11
|
+
* The retry function
|
|
12
|
+
*/
|
|
13
|
+
retry: (opts: RetryFnOptions<TInferrable>) => boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface RetryFnOptions<TInferrable extends InferrableClientTypes> {
|
|
17
|
+
/**
|
|
18
|
+
* The operation that failed
|
|
19
|
+
*/
|
|
20
|
+
op: Operation;
|
|
21
|
+
/**
|
|
22
|
+
* The error that occurred
|
|
23
|
+
*/
|
|
24
|
+
error: TRPCClientError<TInferrable>;
|
|
25
|
+
/**
|
|
26
|
+
* The number of attempts that have been made (including the first call)
|
|
27
|
+
*/
|
|
28
|
+
attempts: number;
|
|
29
|
+
}
|
|
7
30
|
|
|
8
31
|
/**
|
|
9
|
-
* @
|
|
32
|
+
* @see https://trpc.io/docs/v11/client/links/retryLink
|
|
10
33
|
*/
|
|
11
|
-
export function retryLink<
|
|
12
|
-
|
|
13
|
-
|
|
34
|
+
export function retryLink<TInferrable extends InferrableClientTypes>(
|
|
35
|
+
opts: RetryLinkOptions<TInferrable>,
|
|
36
|
+
): TRPCLink<TInferrable> {
|
|
14
37
|
// initialized config
|
|
15
38
|
return () => {
|
|
16
39
|
// initialized in app
|
|
17
40
|
return ({ op, next }) => {
|
|
18
41
|
// initialized for request
|
|
19
42
|
return observable((observer) => {
|
|
20
|
-
let next$: Unsubscribable
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
next$?.unsubscribe();
|
|
43
|
+
let next$: Unsubscribable;
|
|
44
|
+
|
|
45
|
+
attempt(1);
|
|
46
|
+
|
|
47
|
+
function attempt(attempts: number) {
|
|
26
48
|
next$ = next(op).subscribe({
|
|
27
49
|
error(error) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
attempt();
|
|
50
|
+
const shouldRetry = opts.retry({
|
|
51
|
+
op,
|
|
52
|
+
attempts,
|
|
53
|
+
error,
|
|
54
|
+
});
|
|
55
|
+
shouldRetry ? attempt(attempts + 1) : observer.error(error);
|
|
34
56
|
},
|
|
35
57
|
next(result) {
|
|
36
58
|
observer.next(result);
|
|
37
59
|
},
|
|
38
60
|
complete() {
|
|
39
|
-
|
|
40
|
-
observer.complete();
|
|
41
|
-
}
|
|
61
|
+
observer.complete();
|
|
42
62
|
},
|
|
43
63
|
});
|
|
44
64
|
}
|
|
45
|
-
attempt();
|
|
46
65
|
return () => {
|
|
47
|
-
|
|
48
|
-
next$?.unsubscribe();
|
|
66
|
+
next$.unsubscribe();
|
|
49
67
|
};
|
|
50
68
|
});
|
|
51
69
|
};
|
|
@@ -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;
|
package/src/links/loggerLink.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
21
|
+
type EnableFnOptions<TRouter extends InferrableClientTypes> =
|
|
19
22
|
| {
|
|
20
23
|
direction: 'down';
|
|
21
|
-
result:
|
|
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:
|
|
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 ||
|
|
203
|
+
(props.result instanceof Error ||
|
|
204
|
+
('error' in props.result.result && props.result.result.error))
|
|
197
205
|
? 'error'
|
|
198
206
|
: 'log';
|
|
199
207
|
|
|
@@ -226,7 +234,9 @@ export function loggerLink<TRouter extends AnyRouter = AnyRouter>(
|
|
|
226
234
|
});
|
|
227
235
|
const requestStartTime = Date.now();
|
|
228
236
|
function logResult(
|
|
229
|
-
result:
|
|
237
|
+
result:
|
|
238
|
+
| OperationResultEnvelope<unknown, TRPCClientError<TRouter>>
|
|
239
|
+
| TRPCClientError<TRouter>,
|
|
230
240
|
) {
|
|
231
241
|
const elapsedMs = Date.now() - requestStartTime;
|
|
232
242
|
|
package/src/links/types.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
} from '@trpc/server/unstable-core-do-not-import';
|
|
8
8
|
import type { ResponseEsque } from '../internals/types';
|
|
9
9
|
import type { TRPCClientError } from '../TRPCClientError';
|
|
10
|
+
import type { TRPCConnectionState } from './internals/subscriptions';
|
|
10
11
|
|
|
11
12
|
export {
|
|
12
13
|
isNonJsonSerializable,
|
|
@@ -58,10 +59,11 @@ export interface TRPCClientRuntime {
|
|
|
58
59
|
/**
|
|
59
60
|
* @internal
|
|
60
61
|
*/
|
|
61
|
-
export interface OperationResultEnvelope<TOutput> {
|
|
62
|
+
export interface OperationResultEnvelope<TOutput, TError> {
|
|
62
63
|
result:
|
|
63
64
|
| TRPCResultMessage<TOutput>['result']
|
|
64
|
-
| TRPCSuccessResponse<TOutput>['result']
|
|
65
|
+
| TRPCSuccessResponse<TOutput>['result']
|
|
66
|
+
| TRPCConnectionState<TError>;
|
|
65
67
|
context?: OperationContext;
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -71,7 +73,10 @@ export interface OperationResultEnvelope<TOutput> {
|
|
|
71
73
|
export type OperationResultObservable<
|
|
72
74
|
TInferrable extends InferrableClientTypes,
|
|
73
75
|
TOutput,
|
|
74
|
-
> = Observable<
|
|
76
|
+
> = Observable<
|
|
77
|
+
OperationResultEnvelope<TOutput, TRPCClientError<TInferrable>>,
|
|
78
|
+
TRPCClientError<TInferrable>
|
|
79
|
+
>;
|
|
75
80
|
|
|
76
81
|
/**
|
|
77
82
|
* @internal
|
|
@@ -79,7 +84,10 @@ export type OperationResultObservable<
|
|
|
79
84
|
export type OperationResultObserver<
|
|
80
85
|
TInferrable extends InferrableClientTypes,
|
|
81
86
|
TOutput,
|
|
82
|
-
> = Observer<
|
|
87
|
+
> = Observer<
|
|
88
|
+
OperationResultEnvelope<TOutput, TRPCClientError<TInferrable>>,
|
|
89
|
+
TRPCClientError<TInferrable>
|
|
90
|
+
>;
|
|
83
91
|
|
|
84
92
|
/**
|
|
85
93
|
* @internal
|
package/src/links/wsLink.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Observer, UnsubscribeFn } from '@trpc/server/observable';
|
|
2
|
-
import { observable } from '@trpc/server/observable';
|
|
2
|
+
import { behaviorSubject, observable } from '@trpc/server/observable';
|
|
3
3
|
import type { TRPCConnectionParamsMessage } from '@trpc/server/rpc';
|
|
4
4
|
import type {
|
|
5
5
|
AnyRouter,
|
|
@@ -16,6 +16,7 @@ import { transformResult } from '@trpc/server/unstable-core-do-not-import';
|
|
|
16
16
|
import { TRPCClientError } from '../TRPCClientError';
|
|
17
17
|
import type { TransformerOptions } from '../unstable-internals';
|
|
18
18
|
import { getTransformer } from '../unstable-internals';
|
|
19
|
+
import type { TRPCConnectionState } from './internals/subscriptions';
|
|
19
20
|
import {
|
|
20
21
|
resultOf,
|
|
21
22
|
type UrlOptionsWithConnectionParams,
|
|
@@ -100,6 +101,13 @@ const lazyDefaults: LazyOptions = {
|
|
|
100
101
|
enabled: false,
|
|
101
102
|
closeMs: 0,
|
|
102
103
|
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @see https://trpc.io/docs/v11/client/links/wsLink
|
|
107
|
+
* @deprecated
|
|
108
|
+
* 🙋♂️ **Contributors needed** to continue supporting WebSockets!
|
|
109
|
+
* See https://github.com/trpc/trpc/issues/6109
|
|
110
|
+
*/
|
|
103
111
|
export function createWSClient(opts: WebSocketClientOptions) {
|
|
104
112
|
const {
|
|
105
113
|
WebSocket: WebSocketImpl = WebSocket,
|
|
@@ -165,12 +173,27 @@ export function createWSClient(opts: WebSocketClientOptions) {
|
|
|
165
173
|
}
|
|
166
174
|
);
|
|
167
175
|
|
|
176
|
+
const initState: TRPCConnectionState<TRPCClientError<AnyRouter>> =
|
|
177
|
+
activeConnection
|
|
178
|
+
? {
|
|
179
|
+
type: 'state',
|
|
180
|
+
state: 'connecting',
|
|
181
|
+
error: null,
|
|
182
|
+
}
|
|
183
|
+
: {
|
|
184
|
+
type: 'state',
|
|
185
|
+
state: 'idle',
|
|
186
|
+
error: null,
|
|
187
|
+
};
|
|
188
|
+
const connectionState =
|
|
189
|
+
behaviorSubject<TRPCConnectionState<TRPCClientError<AnyRouter>>>(initState);
|
|
190
|
+
|
|
168
191
|
/**
|
|
169
192
|
* tries to send the list of messages
|
|
170
193
|
*/
|
|
171
194
|
function dispatch() {
|
|
172
195
|
if (!activeConnection) {
|
|
173
|
-
|
|
196
|
+
reconnect(null);
|
|
174
197
|
return;
|
|
175
198
|
}
|
|
176
199
|
// using a timeout to batch messages
|
|
@@ -196,12 +219,13 @@ export function createWSClient(opts: WebSocketClientOptions) {
|
|
|
196
219
|
startLazyDisconnectTimer();
|
|
197
220
|
});
|
|
198
221
|
}
|
|
199
|
-
function tryReconnect() {
|
|
222
|
+
function tryReconnect(cause: Error | null) {
|
|
200
223
|
if (!!connectTimer) {
|
|
201
224
|
return;
|
|
202
225
|
}
|
|
226
|
+
|
|
203
227
|
const timeout = retryDelayFn(connectAttempt++);
|
|
204
|
-
reconnectInMs(timeout);
|
|
228
|
+
reconnectInMs(timeout, cause);
|
|
205
229
|
}
|
|
206
230
|
function hasPendingRequests(conn?: Connection) {
|
|
207
231
|
const requests = Object.values(pendingRequests);
|
|
@@ -211,20 +235,31 @@ export function createWSClient(opts: WebSocketClientOptions) {
|
|
|
211
235
|
return requests.some((req) => req.connection === conn);
|
|
212
236
|
}
|
|
213
237
|
|
|
214
|
-
function reconnect() {
|
|
238
|
+
function reconnect(cause: Error | null) {
|
|
215
239
|
if (lazyOpts.enabled && !hasPendingRequests()) {
|
|
216
|
-
// Skip reconnecting if there
|
|
240
|
+
// Skip reconnecting if there aren't pending requests and we're in lazy mode
|
|
217
241
|
return;
|
|
218
242
|
}
|
|
219
243
|
const oldConnection = activeConnection;
|
|
220
244
|
activeConnection = createConnection();
|
|
221
245
|
oldConnection && closeIfNoPending(oldConnection);
|
|
246
|
+
|
|
247
|
+
const currentState = connectionState.get();
|
|
248
|
+
if (currentState.state !== 'connecting') {
|
|
249
|
+
connectionState.next({
|
|
250
|
+
type: 'state',
|
|
251
|
+
state: 'connecting',
|
|
252
|
+
error: cause ? TRPCClientError.from(cause) : null,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
222
255
|
}
|
|
223
|
-
function reconnectInMs(ms: number) {
|
|
256
|
+
function reconnectInMs(ms: number, cause: Error | null) {
|
|
224
257
|
if (connectTimer) {
|
|
225
258
|
return;
|
|
226
259
|
}
|
|
227
|
-
connectTimer = setTimeout(
|
|
260
|
+
connectTimer = setTimeout(() => {
|
|
261
|
+
reconnect(cause);
|
|
262
|
+
}, ms);
|
|
228
263
|
}
|
|
229
264
|
|
|
230
265
|
function closeIfNoPending(conn: Connection) {
|
|
@@ -255,9 +290,14 @@ export function createWSClient(opts: WebSocketClientOptions) {
|
|
|
255
290
|
return;
|
|
256
291
|
}
|
|
257
292
|
|
|
258
|
-
if (!hasPendingRequests(
|
|
293
|
+
if (!hasPendingRequests()) {
|
|
259
294
|
activeConnection.ws?.close();
|
|
260
295
|
activeConnection = null;
|
|
296
|
+
connectionState.next({
|
|
297
|
+
type: 'state',
|
|
298
|
+
state: 'idle',
|
|
299
|
+
error: null,
|
|
300
|
+
});
|
|
261
301
|
}
|
|
262
302
|
}, lazyOpts.closeMs);
|
|
263
303
|
};
|
|
@@ -272,18 +312,31 @@ export function createWSClient(opts: WebSocketClientOptions) {
|
|
|
272
312
|
|
|
273
313
|
clearTimeout(lazyDisconnectTimer);
|
|
274
314
|
|
|
275
|
-
|
|
315
|
+
function destroy() {
|
|
316
|
+
const noop = () => {
|
|
317
|
+
// no-op
|
|
318
|
+
};
|
|
319
|
+
const { ws } = self;
|
|
320
|
+
if (ws) {
|
|
321
|
+
ws.onclose = noop;
|
|
322
|
+
ws.onerror = noop;
|
|
323
|
+
ws.onmessage = noop;
|
|
324
|
+
ws.onopen = noop;
|
|
325
|
+
|
|
326
|
+
ws.close();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
self.state = 'closed';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const onCloseOrError = (cause: Error | null) => {
|
|
276
333
|
clearTimeout(pingTimeout);
|
|
277
334
|
clearTimeout(pongTimeout);
|
|
278
335
|
|
|
279
|
-
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
(self as Connection).state = 'closed';
|
|
336
|
+
self.state = 'closed';
|
|
284
337
|
if (activeConnection === self) {
|
|
285
338
|
// connection might have been replaced already
|
|
286
|
-
tryReconnect();
|
|
339
|
+
tryReconnect(cause);
|
|
287
340
|
}
|
|
288
341
|
|
|
289
342
|
for (const [key, req] of Object.entries(pendingRequests)) {
|
|
@@ -299,29 +352,18 @@ export function createWSClient(opts: WebSocketClientOptions) {
|
|
|
299
352
|
// Queries and mutations will error if interrupted
|
|
300
353
|
delete pendingRequests[key];
|
|
301
354
|
req.callbacks.error?.(
|
|
302
|
-
TRPCClientError.from(
|
|
303
|
-
new TRPCWebSocketClosedError('WebSocket closed prematurely'),
|
|
304
|
-
),
|
|
355
|
+
TRPCClientError.from(cause ?? new TRPCWebSocketClosedError()),
|
|
305
356
|
);
|
|
306
357
|
}
|
|
307
358
|
}
|
|
308
359
|
};
|
|
309
360
|
|
|
310
|
-
const onClose = (code: number) => {
|
|
311
|
-
const wasOpen = self.state === 'open';
|
|
312
|
-
onCloseOrError();
|
|
313
|
-
|
|
314
|
-
if (wasOpen) {
|
|
315
|
-
opts.onClose?.({ code });
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
|
|
319
361
|
const onError = (evt?: Event) => {
|
|
320
|
-
onCloseOrError();
|
|
362
|
+
onCloseOrError(new TRPCWebSocketClosedError({ cause: evt }));
|
|
321
363
|
opts.onError?.(evt);
|
|
322
364
|
};
|
|
323
|
-
|
|
324
|
-
|
|
365
|
+
|
|
366
|
+
function connect(url: string) {
|
|
325
367
|
if (opts.connectionParams) {
|
|
326
368
|
// append `?connectionParams=1` when connection params are used
|
|
327
369
|
const prefix = url.includes('?') ? '&' : '?';
|
|
@@ -334,7 +376,7 @@ export function createWSClient(opts: WebSocketClientOptions) {
|
|
|
334
376
|
clearTimeout(connectTimer);
|
|
335
377
|
connectTimer = undefined;
|
|
336
378
|
|
|
337
|
-
ws.
|
|
379
|
+
ws.onopen = () => {
|
|
338
380
|
async function sendConnectionParams() {
|
|
339
381
|
if (!opts.connectionParams) {
|
|
340
382
|
return;
|
|
@@ -356,8 +398,11 @@ export function createWSClient(opts: WebSocketClientOptions) {
|
|
|
356
398
|
const schedulePing = () => {
|
|
357
399
|
const schedulePongTimeout = () => {
|
|
358
400
|
pongTimeout = setTimeout(() => {
|
|
359
|
-
|
|
360
|
-
|
|
401
|
+
const wasOpen = self.state === 'open';
|
|
402
|
+
destroy();
|
|
403
|
+
if (wasOpen) {
|
|
404
|
+
opts.onClose?.();
|
|
405
|
+
}
|
|
361
406
|
}, pongTimeoutMs);
|
|
362
407
|
};
|
|
363
408
|
pingTimeout = setTimeout(() => {
|
|
@@ -385,25 +430,40 @@ export function createWSClient(opts: WebSocketClientOptions) {
|
|
|
385
430
|
connectAttempt = 0;
|
|
386
431
|
self.state = 'open';
|
|
387
432
|
|
|
433
|
+
// Update connection state
|
|
434
|
+
connectionState.next({
|
|
435
|
+
type: 'state',
|
|
436
|
+
state: 'pending',
|
|
437
|
+
error: null,
|
|
438
|
+
});
|
|
439
|
+
|
|
388
440
|
opts.onOpen?.();
|
|
389
441
|
dispatch();
|
|
390
|
-
}).catch((cause) => {
|
|
442
|
+
}).catch((cause: unknown) => {
|
|
391
443
|
ws.close(
|
|
392
444
|
// "Status codes in the range 3000-3999 are reserved for use by libraries, frameworks, and applications"
|
|
393
445
|
3000,
|
|
394
|
-
cause,
|
|
395
446
|
);
|
|
396
|
-
|
|
447
|
+
onCloseOrError(
|
|
448
|
+
new TRPCWebSocketClosedError({
|
|
449
|
+
message: 'Initialization error',
|
|
450
|
+
cause,
|
|
451
|
+
}),
|
|
452
|
+
);
|
|
397
453
|
});
|
|
398
|
-
}
|
|
399
|
-
ws.
|
|
454
|
+
};
|
|
455
|
+
ws.onerror = onError;
|
|
400
456
|
const handleIncomingRequest = (req: TRPCClientIncomingRequest) => {
|
|
401
457
|
if (self !== activeConnection) {
|
|
402
458
|
return;
|
|
403
459
|
}
|
|
404
460
|
|
|
405
461
|
if (req.method === 'reconnect') {
|
|
406
|
-
reconnect(
|
|
462
|
+
reconnect(
|
|
463
|
+
new TRPCWebSocketClosedError({
|
|
464
|
+
message: 'Server requested reconnect',
|
|
465
|
+
}),
|
|
466
|
+
);
|
|
407
467
|
// notify subscribers
|
|
408
468
|
for (const pendingReq of Object.values(pendingRequests)) {
|
|
409
469
|
if (pendingReq.type === 'subscription') {
|
|
@@ -445,7 +505,8 @@ export function createWSClient(opts: WebSocketClientOptions) {
|
|
|
445
505
|
}
|
|
446
506
|
};
|
|
447
507
|
|
|
448
|
-
ws.
|
|
508
|
+
ws.onmessage = (event) => {
|
|
509
|
+
const { data } = event;
|
|
449
510
|
if (data === 'PONG') {
|
|
450
511
|
return;
|
|
451
512
|
}
|
|
@@ -466,18 +527,25 @@ export function createWSClient(opts: WebSocketClientOptions) {
|
|
|
466
527
|
// when receiving a message, we close old connection that has no pending requests
|
|
467
528
|
closeIfNoPending(self);
|
|
468
529
|
}
|
|
469
|
-
}
|
|
530
|
+
};
|
|
470
531
|
|
|
471
|
-
ws.
|
|
532
|
+
ws.onclose = (event) => {
|
|
472
533
|
const wasOpen = self.state === 'open';
|
|
473
534
|
|
|
474
|
-
|
|
535
|
+
destroy();
|
|
536
|
+
onCloseOrError(new TRPCWebSocketClosedError({ cause: event }));
|
|
475
537
|
|
|
476
538
|
if (wasOpen) {
|
|
477
|
-
opts.onClose?.(
|
|
539
|
+
opts.onClose?.(event);
|
|
478
540
|
}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
Promise.resolve(resultOf(opts.url))
|
|
545
|
+
.then(connect)
|
|
546
|
+
.catch(() => {
|
|
547
|
+
onCloseOrError(new Error('Failed to resolve url'));
|
|
479
548
|
});
|
|
480
|
-
}).catch(onError);
|
|
481
549
|
return self;
|
|
482
550
|
}
|
|
483
551
|
|
|
@@ -527,6 +595,7 @@ export function createWSClient(opts: WebSocketClientOptions) {
|
|
|
527
595
|
startLazyDisconnectTimer();
|
|
528
596
|
};
|
|
529
597
|
}
|
|
598
|
+
|
|
530
599
|
return {
|
|
531
600
|
close: () => {
|
|
532
601
|
connectAttempt = 0;
|
|
@@ -538,7 +607,9 @@ export function createWSClient(opts: WebSocketClientOptions) {
|
|
|
538
607
|
// close pending requests that aren't attached to a connection yet
|
|
539
608
|
req.callbacks.error(
|
|
540
609
|
TRPCClientError.from(
|
|
541
|
-
new
|
|
610
|
+
new TRPCWebSocketClosedError({
|
|
611
|
+
message: 'Closed before connection was established',
|
|
612
|
+
}),
|
|
542
613
|
),
|
|
543
614
|
);
|
|
544
615
|
}
|
|
@@ -556,16 +627,37 @@ export function createWSClient(opts: WebSocketClientOptions) {
|
|
|
556
627
|
* Reconnect to the WebSocket server
|
|
557
628
|
*/
|
|
558
629
|
reconnect,
|
|
630
|
+
connectionState: connectionState,
|
|
559
631
|
};
|
|
560
632
|
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* @see https://trpc.io/docs/v11/client/links/wsLink
|
|
636
|
+
* @deprecated
|
|
637
|
+
* 🙋♂️ **Contributors needed** to continue supporting WebSockets!
|
|
638
|
+
* See https://github.com/trpc/trpc/issues/6109
|
|
639
|
+
*/
|
|
561
640
|
export type TRPCWebSocketClient = ReturnType<typeof createWSClient>;
|
|
562
641
|
|
|
642
|
+
/**
|
|
643
|
+
* @see https://trpc.io/docs/v11/client/links/wsLink
|
|
644
|
+
* @deprecated
|
|
645
|
+
* 🙋♂️ **Contributors needed** to continue supporting WebSockets!
|
|
646
|
+
* See https://github.com/trpc/trpc/issues/6109
|
|
647
|
+
*/
|
|
563
648
|
export type WebSocketLinkOptions<TRouter extends AnyRouter> = {
|
|
564
649
|
client: TRPCWebSocketClient;
|
|
565
650
|
} & TransformerOptions<inferClientTypes<TRouter>>;
|
|
566
651
|
class TRPCWebSocketClosedError extends Error {
|
|
567
|
-
constructor(message
|
|
568
|
-
super(
|
|
652
|
+
constructor(opts?: { cause?: unknown; message?: string }) {
|
|
653
|
+
super(
|
|
654
|
+
opts?.message ?? 'WebSocket closed',
|
|
655
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
656
|
+
// @ts-ignore https://github.com/tc39/proposal-error-cause
|
|
657
|
+
{
|
|
658
|
+
cause: opts?.cause,
|
|
659
|
+
},
|
|
660
|
+
);
|
|
569
661
|
this.name = 'TRPCWebSocketClosedError';
|
|
570
662
|
Object.setPrototypeOf(this, TRPCWebSocketClosedError.prototype);
|
|
571
663
|
}
|
|
@@ -573,6 +665,9 @@ class TRPCWebSocketClosedError extends Error {
|
|
|
573
665
|
|
|
574
666
|
/**
|
|
575
667
|
* @see https://trpc.io/docs/v11/client/links/wsLink
|
|
668
|
+
* @deprecated
|
|
669
|
+
* 🙋♂️ **Contributors needed** to continue supporting WebSockets!
|
|
670
|
+
* See https://github.com/trpc/trpc/issues/6109
|
|
576
671
|
*/
|
|
577
672
|
export function wsLink<TRouter extends AnyRouter>(
|
|
578
673
|
opts: WebSocketLinkOptions<TRouter>,
|
|
@@ -586,18 +681,29 @@ export function wsLink<TRouter extends AnyRouter>(
|
|
|
586
681
|
|
|
587
682
|
const input = transformer.input.serialize(op.input);
|
|
588
683
|
|
|
589
|
-
const
|
|
684
|
+
const connState =
|
|
685
|
+
type === 'subscription'
|
|
686
|
+
? client.connectionState.subscribe({
|
|
687
|
+
next(result) {
|
|
688
|
+
observer.next({
|
|
689
|
+
result,
|
|
690
|
+
context,
|
|
691
|
+
});
|
|
692
|
+
},
|
|
693
|
+
})
|
|
694
|
+
: null;
|
|
695
|
+
const unsubscribeRequest = client.request({
|
|
590
696
|
op: { type, path, input, id, context, signal: null },
|
|
591
697
|
callbacks: {
|
|
592
698
|
error(err) {
|
|
593
|
-
observer.error(err
|
|
594
|
-
|
|
699
|
+
observer.error(err);
|
|
700
|
+
unsubscribeRequest();
|
|
595
701
|
},
|
|
596
702
|
complete() {
|
|
597
703
|
observer.complete();
|
|
598
704
|
},
|
|
599
|
-
next(
|
|
600
|
-
const transformed = transformResult(
|
|
705
|
+
next(event) {
|
|
706
|
+
const transformed = transformResult(event, transformer.output);
|
|
601
707
|
|
|
602
708
|
if (!transformed.ok) {
|
|
603
709
|
observer.error(TRPCClientError.from(transformed.error));
|
|
@@ -610,7 +716,7 @@ export function wsLink<TRouter extends AnyRouter>(
|
|
|
610
716
|
if (op.type !== 'subscription') {
|
|
611
717
|
// if it isn't a subscription we don't care about next response
|
|
612
718
|
|
|
613
|
-
|
|
719
|
+
unsubscribeRequest();
|
|
614
720
|
observer.complete();
|
|
615
721
|
}
|
|
616
722
|
},
|
|
@@ -618,7 +724,8 @@ export function wsLink<TRouter extends AnyRouter>(
|
|
|
618
724
|
lastEventId: undefined,
|
|
619
725
|
});
|
|
620
726
|
return () => {
|
|
621
|
-
|
|
727
|
+
unsubscribeRequest();
|
|
728
|
+
connState?.unsubscribe();
|
|
622
729
|
};
|
|
623
730
|
});
|
|
624
731
|
};
|
package/src/links.ts
CHANGED
|
@@ -8,7 +8,7 @@ export * from './links/loggerLink';
|
|
|
8
8
|
export * from './links/splitLink';
|
|
9
9
|
export * from './links/wsLink';
|
|
10
10
|
export * from './links/httpSubscriptionLink';
|
|
11
|
+
export * from './links/internals/retryLink';
|
|
11
12
|
|
|
12
13
|
// These are not public (yet) as we get this functionality from tanstack query
|
|
13
|
-
// export * from './links/internals/retryLink';
|
|
14
14
|
// export * from './links/internals/dedupeLink';
|