@trpc/client 11.0.0-rc.772 → 11.0.0-rc.781
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/bundle-analysis.json +132 -39
- package/dist/index.js +3 -2
- package/dist/index.mjs +2 -1
- package/dist/links/wsLink/createWsClient.d.ts +6 -0
- package/dist/links/wsLink/createWsClient.d.ts.map +1 -0
- package/dist/links/wsLink/createWsClient.js +9 -0
- package/dist/links/wsLink/createWsClient.mjs +7 -0
- package/dist/links/wsLink/wsClient/options.d.ts +79 -0
- package/dist/links/wsLink/wsClient/options.d.ts.map +1 -0
- package/dist/links/wsLink/wsClient/options.js +22 -0
- package/dist/links/wsLink/wsClient/options.mjs +18 -0
- package/dist/links/wsLink/wsClient/requestManager.d.ts +102 -0
- package/dist/links/wsLink/wsClient/requestManager.d.ts.map +1 -0
- package/dist/links/wsLink/wsClient/requestManager.js +138 -0
- package/dist/links/wsLink/wsClient/requestManager.mjs +136 -0
- package/dist/links/wsLink/wsClient/utils.d.ts +38 -0
- package/dist/links/wsLink/wsClient/utils.d.ts.map +1 -0
- package/dist/links/wsLink/wsClient/utils.js +94 -0
- package/dist/links/wsLink/wsClient/utils.mjs +88 -0
- package/dist/links/wsLink/wsClient/wsClient.d.ts +85 -0
- package/dist/links/wsLink/wsClient/wsClient.d.ts.map +1 -0
- package/dist/links/wsLink/wsClient/wsClient.js +331 -0
- package/dist/links/wsLink/wsClient/wsClient.mjs +329 -0
- package/dist/links/wsLink/wsClient/wsConnection.d.ts +79 -0
- package/dist/links/wsLink/wsClient/wsConnection.d.ts.map +1 -0
- package/dist/links/wsLink/wsClient/wsConnection.js +181 -0
- package/dist/links/wsLink/wsClient/wsConnection.mjs +178 -0
- package/dist/links/wsLink/wsLink.d.ts +11 -0
- package/dist/links/wsLink/wsLink.d.ts.map +1 -0
- package/dist/links/wsLink/wsLink.js +35 -0
- package/dist/links/wsLink/wsLink.mjs +32 -0
- package/dist/links.d.ts +1 -1
- package/dist/links.d.ts.map +1 -1
- package/links/wsLink/wsLink/index.d.ts +1 -0
- package/links/wsLink/wsLink/index.js +1 -0
- package/package.json +8 -8
- package/src/links/wsLink/createWsClient.ts +10 -0
- package/src/links/wsLink/wsClient/options.ts +91 -0
- package/src/links/wsLink/wsClient/requestManager.ts +174 -0
- package/src/links/wsLink/wsClient/utils.ts +94 -0
- package/src/links/wsLink/wsClient/wsClient.ts +441 -0
- package/src/links/wsLink/wsClient/wsConnection.ts +230 -0
- package/src/links/wsLink/wsLink.ts +55 -0
- package/src/links.ts +1 -1
- package/dist/links/wsLink.d.ts +0 -125
- package/dist/links/wsLink.d.ts.map +0 -1
- package/dist/links/wsLink.js +0 -498
- package/dist/links/wsLink.mjs +0 -495
- package/links/wsLink/index.d.ts +0 -1
- package/links/wsLink/index.js +0 -1
- package/src/links/wsLink.ts +0 -737
package/src/links/wsLink.ts
DELETED
|
@@ -1,737 +0,0 @@
|
|
|
1
|
-
import type { Observer, UnsubscribeFn } from '@trpc/server/observable';
|
|
2
|
-
import { behaviorSubject, observable } from '@trpc/server/observable';
|
|
3
|
-
import type { TRPCConnectionParamsMessage } from '@trpc/server/rpc';
|
|
4
|
-
import type {
|
|
5
|
-
AnyRouter,
|
|
6
|
-
inferClientTypes,
|
|
7
|
-
inferRouterError,
|
|
8
|
-
ProcedureType,
|
|
9
|
-
TRPCClientIncomingMessage,
|
|
10
|
-
TRPCClientIncomingRequest,
|
|
11
|
-
TRPCClientOutgoingMessage,
|
|
12
|
-
TRPCRequestMessage,
|
|
13
|
-
TRPCResponseMessage,
|
|
14
|
-
} from '@trpc/server/unstable-core-do-not-import';
|
|
15
|
-
import { transformResult } from '@trpc/server/unstable-core-do-not-import';
|
|
16
|
-
import { TRPCClientError } from '../TRPCClientError';
|
|
17
|
-
import type { TransformerOptions } from '../unstable-internals';
|
|
18
|
-
import { getTransformer } from '../unstable-internals';
|
|
19
|
-
import type { TRPCConnectionState } from './internals/subscriptions';
|
|
20
|
-
import {
|
|
21
|
-
resultOf,
|
|
22
|
-
type UrlOptionsWithConnectionParams,
|
|
23
|
-
} from './internals/urlWithConnectionParams';
|
|
24
|
-
import type { Operation, TRPCLink } from './types';
|
|
25
|
-
|
|
26
|
-
const run = <TResult>(fn: () => TResult): TResult => fn();
|
|
27
|
-
|
|
28
|
-
type WSCallbackResult<TRouter extends AnyRouter, TOutput> = TRPCResponseMessage<
|
|
29
|
-
TOutput,
|
|
30
|
-
inferRouterError<TRouter>
|
|
31
|
-
>;
|
|
32
|
-
|
|
33
|
-
type WSCallbackObserver<TRouter extends AnyRouter, TOutput> = Observer<
|
|
34
|
-
WSCallbackResult<TRouter, TOutput>,
|
|
35
|
-
TRPCClientError<TRouter>
|
|
36
|
-
>;
|
|
37
|
-
|
|
38
|
-
const exponentialBackoff = (attemptIndex: number) =>
|
|
39
|
-
attemptIndex === 0 ? 0 : Math.min(1000 * 2 ** attemptIndex, 30000);
|
|
40
|
-
|
|
41
|
-
export interface WebSocketClientOptions extends UrlOptionsWithConnectionParams {
|
|
42
|
-
/**
|
|
43
|
-
* Ponyfill which WebSocket implementation to use
|
|
44
|
-
*/
|
|
45
|
-
WebSocket?: typeof WebSocket;
|
|
46
|
-
/**
|
|
47
|
-
* The number of milliseconds before a reconnect is attempted.
|
|
48
|
-
* @default {@link exponentialBackoff}
|
|
49
|
-
*/
|
|
50
|
-
retryDelayMs?: typeof exponentialBackoff;
|
|
51
|
-
/**
|
|
52
|
-
* Triggered when a WebSocket connection is established
|
|
53
|
-
*/
|
|
54
|
-
onOpen?: () => void;
|
|
55
|
-
/**
|
|
56
|
-
* Triggered when a WebSocket connection encounters an error
|
|
57
|
-
*/
|
|
58
|
-
onError?: (evt?: Event) => void;
|
|
59
|
-
/**
|
|
60
|
-
* Triggered when a WebSocket connection is closed
|
|
61
|
-
*/
|
|
62
|
-
onClose?: (cause?: { code?: number }) => void;
|
|
63
|
-
/**
|
|
64
|
-
* Lazy mode will close the WebSocket automatically after a period of inactivity (no messages sent or received and no pending requests)
|
|
65
|
-
*/
|
|
66
|
-
lazy?: {
|
|
67
|
-
/**
|
|
68
|
-
* Enable lazy mode
|
|
69
|
-
* @default false
|
|
70
|
-
*/
|
|
71
|
-
enabled: boolean;
|
|
72
|
-
/**
|
|
73
|
-
* Close the WebSocket after this many milliseconds
|
|
74
|
-
* @default 0
|
|
75
|
-
*/
|
|
76
|
-
closeMs: number;
|
|
77
|
-
};
|
|
78
|
-
/**
|
|
79
|
-
* Send ping messages to the server and kill the connection if no pong message is returned
|
|
80
|
-
*/
|
|
81
|
-
keepAlive?: {
|
|
82
|
-
/**
|
|
83
|
-
* @default false
|
|
84
|
-
*/
|
|
85
|
-
enabled: boolean;
|
|
86
|
-
/**
|
|
87
|
-
* Send a ping message every this many milliseconds
|
|
88
|
-
* @default 5_000
|
|
89
|
-
*/
|
|
90
|
-
intervalMs?: number;
|
|
91
|
-
/**
|
|
92
|
-
* Close the WebSocket after this many milliseconds if the server does not respond
|
|
93
|
-
* @default 1_000
|
|
94
|
-
*/
|
|
95
|
-
pongTimeoutMs?: number;
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
type LazyOptions = Required<NonNullable<WebSocketClientOptions['lazy']>>;
|
|
100
|
-
const lazyDefaults: LazyOptions = {
|
|
101
|
-
enabled: false,
|
|
102
|
-
closeMs: 0,
|
|
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
|
-
*/
|
|
111
|
-
export function createWSClient(opts: WebSocketClientOptions) {
|
|
112
|
-
const {
|
|
113
|
-
WebSocket: WebSocketImpl = WebSocket,
|
|
114
|
-
retryDelayMs: retryDelayFn = exponentialBackoff,
|
|
115
|
-
} = opts;
|
|
116
|
-
const lazyOpts: LazyOptions = {
|
|
117
|
-
...lazyDefaults,
|
|
118
|
-
...opts.lazy,
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
/* istanbul ignore next -- @preserve */
|
|
122
|
-
if (!WebSocketImpl) {
|
|
123
|
-
throw new Error(
|
|
124
|
-
"No WebSocket implementation found - you probably don't want to use this on the server, but if you do you need to pass a `WebSocket`-ponyfill",
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* outgoing messages buffer whilst not open
|
|
129
|
-
*/
|
|
130
|
-
let outgoing: TRPCClientOutgoingMessage[] = [];
|
|
131
|
-
/**
|
|
132
|
-
* pending outgoing requests that are awaiting callback
|
|
133
|
-
*/
|
|
134
|
-
type TCallbacks = WSCallbackObserver<AnyRouter, unknown>;
|
|
135
|
-
type WsRequest = {
|
|
136
|
-
/**
|
|
137
|
-
* Reference to the WebSocket instance this request was made to
|
|
138
|
-
*/
|
|
139
|
-
connection: Connection | null;
|
|
140
|
-
type: ProcedureType;
|
|
141
|
-
callbacks: TCallbacks;
|
|
142
|
-
op: Operation;
|
|
143
|
-
/**
|
|
144
|
-
* The last event id that the client has received
|
|
145
|
-
*/
|
|
146
|
-
lastEventId: string | undefined;
|
|
147
|
-
};
|
|
148
|
-
const pendingRequests: Record<number | string, WsRequest> =
|
|
149
|
-
Object.create(null);
|
|
150
|
-
let connectAttempt = 0;
|
|
151
|
-
let connectTimer: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
152
|
-
let connectionIndex = 0;
|
|
153
|
-
let lazyDisconnectTimer: ReturnType<typeof setTimeout> | undefined =
|
|
154
|
-
undefined;
|
|
155
|
-
let activeConnection: null | Connection = lazyOpts.enabled
|
|
156
|
-
? null
|
|
157
|
-
: createConnection();
|
|
158
|
-
|
|
159
|
-
type Connection = {
|
|
160
|
-
id: number;
|
|
161
|
-
} & (
|
|
162
|
-
| {
|
|
163
|
-
state: 'open';
|
|
164
|
-
ws: WebSocket;
|
|
165
|
-
}
|
|
166
|
-
| {
|
|
167
|
-
state: 'closed';
|
|
168
|
-
ws: WebSocket;
|
|
169
|
-
}
|
|
170
|
-
| {
|
|
171
|
-
state: 'connecting';
|
|
172
|
-
ws?: WebSocket;
|
|
173
|
-
}
|
|
174
|
-
);
|
|
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
|
-
|
|
191
|
-
/**
|
|
192
|
-
* tries to send the list of messages
|
|
193
|
-
*/
|
|
194
|
-
function dispatch() {
|
|
195
|
-
if (!activeConnection) {
|
|
196
|
-
reconnect(null);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
// using a timeout to batch messages
|
|
200
|
-
setTimeout(() => {
|
|
201
|
-
if (activeConnection?.state !== 'open') {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
for (const pending of Object.values(pendingRequests)) {
|
|
205
|
-
if (!pending.connection) {
|
|
206
|
-
pending.connection = activeConnection;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (outgoing.length === 1) {
|
|
210
|
-
// single send
|
|
211
|
-
activeConnection.ws.send(JSON.stringify(outgoing.pop()));
|
|
212
|
-
} else {
|
|
213
|
-
// batch send
|
|
214
|
-
activeConnection.ws.send(JSON.stringify(outgoing));
|
|
215
|
-
}
|
|
216
|
-
// clear
|
|
217
|
-
outgoing = [];
|
|
218
|
-
|
|
219
|
-
startLazyDisconnectTimer();
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
function tryReconnect(cause: Error | null) {
|
|
223
|
-
if (!!connectTimer) {
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const timeout = retryDelayFn(connectAttempt++);
|
|
228
|
-
reconnectInMs(timeout, cause);
|
|
229
|
-
}
|
|
230
|
-
function hasPendingRequests(conn?: Connection) {
|
|
231
|
-
const requests = Object.values(pendingRequests);
|
|
232
|
-
if (!conn) {
|
|
233
|
-
return requests.length > 0;
|
|
234
|
-
}
|
|
235
|
-
return requests.some((req) => req.connection === conn);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function reconnect(cause: Error | null) {
|
|
239
|
-
if (lazyOpts.enabled && !hasPendingRequests()) {
|
|
240
|
-
// Skip reconnecting if there aren't pending requests and we're in lazy mode
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
const oldConnection = activeConnection;
|
|
244
|
-
activeConnection = createConnection();
|
|
245
|
-
if (oldConnection) {
|
|
246
|
-
closeIfNoPending(oldConnection);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const currentState = connectionState.get();
|
|
250
|
-
if (currentState.state !== 'connecting') {
|
|
251
|
-
connectionState.next({
|
|
252
|
-
type: 'state',
|
|
253
|
-
state: 'connecting',
|
|
254
|
-
error: cause ? TRPCClientError.from(cause) : null,
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
function reconnectInMs(ms: number, cause: Error | null) {
|
|
259
|
-
if (connectTimer) {
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
connectTimer = setTimeout(() => {
|
|
263
|
-
reconnect(cause);
|
|
264
|
-
}, ms);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function closeIfNoPending(conn: Connection) {
|
|
268
|
-
// disconnect as soon as there are are no pending requests
|
|
269
|
-
if (!hasPendingRequests(conn)) {
|
|
270
|
-
conn.ws?.close();
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
function resumeSubscriptionOnReconnect(req: WsRequest) {
|
|
274
|
-
if (outgoing.some((r) => r.id === req.op.id)) {
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
request({
|
|
278
|
-
op: req.op,
|
|
279
|
-
callbacks: req.callbacks,
|
|
280
|
-
lastEventId: req.lastEventId,
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const startLazyDisconnectTimer = () => {
|
|
285
|
-
if (!lazyOpts.enabled) {
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
clearTimeout(lazyDisconnectTimer);
|
|
290
|
-
lazyDisconnectTimer = setTimeout(() => {
|
|
291
|
-
if (!activeConnection) {
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (!hasPendingRequests()) {
|
|
296
|
-
activeConnection.ws?.close();
|
|
297
|
-
activeConnection = null;
|
|
298
|
-
connectionState.next({
|
|
299
|
-
type: 'state',
|
|
300
|
-
state: 'idle',
|
|
301
|
-
error: null,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
}, lazyOpts.closeMs);
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
function createConnection(): Connection {
|
|
308
|
-
let pingTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
309
|
-
let pongTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
310
|
-
const self: Connection = {
|
|
311
|
-
id: ++connectionIndex,
|
|
312
|
-
state: 'connecting',
|
|
313
|
-
} as Connection;
|
|
314
|
-
|
|
315
|
-
clearTimeout(lazyDisconnectTimer);
|
|
316
|
-
|
|
317
|
-
function destroy() {
|
|
318
|
-
const noop = () => {
|
|
319
|
-
// no-op
|
|
320
|
-
};
|
|
321
|
-
const { ws } = self;
|
|
322
|
-
if (ws) {
|
|
323
|
-
ws.onclose = noop;
|
|
324
|
-
ws.onerror = noop;
|
|
325
|
-
ws.onmessage = noop;
|
|
326
|
-
ws.onopen = noop;
|
|
327
|
-
|
|
328
|
-
ws.close();
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
self.state = 'closed';
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const onCloseOrError = (cause: Error | null) => {
|
|
335
|
-
clearTimeout(pingTimeout);
|
|
336
|
-
clearTimeout(pongTimeout);
|
|
337
|
-
|
|
338
|
-
self.state = 'closed';
|
|
339
|
-
if (activeConnection === self) {
|
|
340
|
-
// connection might have been replaced already
|
|
341
|
-
tryReconnect(cause);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
for (const [key, req] of Object.entries(pendingRequests)) {
|
|
345
|
-
if (req.connection !== self) {
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// The connection was closed either unexpectedly or because of a reconnect
|
|
350
|
-
if (req.type === 'subscription') {
|
|
351
|
-
// Subscriptions will resume after we've reconnected
|
|
352
|
-
resumeSubscriptionOnReconnect(req);
|
|
353
|
-
} else {
|
|
354
|
-
// Queries and mutations will error if interrupted
|
|
355
|
-
delete pendingRequests[key];
|
|
356
|
-
req.callbacks.error?.(
|
|
357
|
-
TRPCClientError.from(cause ?? new TRPCWebSocketClosedError()),
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
const onError = (evt?: Event) => {
|
|
364
|
-
onCloseOrError(new TRPCWebSocketClosedError({ cause: evt }));
|
|
365
|
-
opts.onError?.(evt);
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
function connect(url: string) {
|
|
369
|
-
if (opts.connectionParams) {
|
|
370
|
-
// append `?connectionParams=1` when connection params are used
|
|
371
|
-
const prefix = url.includes('?') ? '&' : '?';
|
|
372
|
-
url += prefix + 'connectionParams=1';
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const ws = new WebSocketImpl(url);
|
|
376
|
-
self.ws = ws;
|
|
377
|
-
|
|
378
|
-
clearTimeout(connectTimer);
|
|
379
|
-
connectTimer = undefined;
|
|
380
|
-
|
|
381
|
-
ws.onopen = () => {
|
|
382
|
-
async function sendConnectionParams() {
|
|
383
|
-
if (!opts.connectionParams) {
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const connectMsg: TRPCConnectionParamsMessage = {
|
|
388
|
-
method: 'connectionParams',
|
|
389
|
-
data: await resultOf(opts.connectionParams),
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
ws.send(JSON.stringify(connectMsg));
|
|
393
|
-
}
|
|
394
|
-
function handleKeepAlive() {
|
|
395
|
-
if (!opts.keepAlive?.enabled) {
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
const { pongTimeoutMs = 1_000, intervalMs = 5_000 } = opts.keepAlive;
|
|
399
|
-
|
|
400
|
-
const schedulePing = () => {
|
|
401
|
-
const schedulePongTimeout = () => {
|
|
402
|
-
pongTimeout = setTimeout(() => {
|
|
403
|
-
const wasOpen = self.state === 'open';
|
|
404
|
-
destroy();
|
|
405
|
-
if (wasOpen) {
|
|
406
|
-
opts.onClose?.();
|
|
407
|
-
}
|
|
408
|
-
}, pongTimeoutMs);
|
|
409
|
-
};
|
|
410
|
-
pingTimeout = setTimeout(() => {
|
|
411
|
-
ws.send('PING');
|
|
412
|
-
schedulePongTimeout();
|
|
413
|
-
}, intervalMs);
|
|
414
|
-
};
|
|
415
|
-
ws.addEventListener('message', () => {
|
|
416
|
-
clearTimeout(pingTimeout);
|
|
417
|
-
clearTimeout(pongTimeout);
|
|
418
|
-
|
|
419
|
-
schedulePing();
|
|
420
|
-
});
|
|
421
|
-
schedulePing();
|
|
422
|
-
}
|
|
423
|
-
run(async () => {
|
|
424
|
-
/* istanbul ignore next -- @preserve */
|
|
425
|
-
if (activeConnection?.ws !== ws) {
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
handleKeepAlive();
|
|
429
|
-
|
|
430
|
-
await sendConnectionParams();
|
|
431
|
-
|
|
432
|
-
connectAttempt = 0;
|
|
433
|
-
self.state = 'open';
|
|
434
|
-
|
|
435
|
-
// Update connection state
|
|
436
|
-
connectionState.next({
|
|
437
|
-
type: 'state',
|
|
438
|
-
state: 'pending',
|
|
439
|
-
error: null,
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
opts.onOpen?.();
|
|
443
|
-
dispatch();
|
|
444
|
-
}).catch((cause: unknown) => {
|
|
445
|
-
ws.close(
|
|
446
|
-
// "Status codes in the range 3000-3999 are reserved for use by libraries, frameworks, and applications"
|
|
447
|
-
3000,
|
|
448
|
-
);
|
|
449
|
-
onCloseOrError(
|
|
450
|
-
new TRPCWebSocketClosedError({
|
|
451
|
-
message: 'Initialization error',
|
|
452
|
-
cause,
|
|
453
|
-
}),
|
|
454
|
-
);
|
|
455
|
-
});
|
|
456
|
-
};
|
|
457
|
-
ws.onerror = onError;
|
|
458
|
-
const handleIncomingRequest = (req: TRPCClientIncomingRequest) => {
|
|
459
|
-
if (self !== activeConnection) {
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (req.method === 'reconnect') {
|
|
464
|
-
reconnect(
|
|
465
|
-
new TRPCWebSocketClosedError({
|
|
466
|
-
message: 'Server requested reconnect',
|
|
467
|
-
}),
|
|
468
|
-
);
|
|
469
|
-
// notify subscribers
|
|
470
|
-
for (const pendingReq of Object.values(pendingRequests)) {
|
|
471
|
-
if (pendingReq.type === 'subscription') {
|
|
472
|
-
resumeSubscriptionOnReconnect(pendingReq);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
};
|
|
477
|
-
const handleIncomingResponse = (data: TRPCResponseMessage) => {
|
|
478
|
-
const req = data.id !== null && pendingRequests[data.id];
|
|
479
|
-
if (!req) {
|
|
480
|
-
// do something?
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
req.callbacks.next?.(data);
|
|
485
|
-
if (self === activeConnection && req.connection !== activeConnection) {
|
|
486
|
-
// gracefully replace old connection with a new connection
|
|
487
|
-
req.connection = self;
|
|
488
|
-
}
|
|
489
|
-
if (req.connection !== self) {
|
|
490
|
-
// the connection has been replaced
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (
|
|
495
|
-
'result' in data &&
|
|
496
|
-
data.result.type === 'data' &&
|
|
497
|
-
typeof data.result.id === 'string'
|
|
498
|
-
) {
|
|
499
|
-
req.lastEventId = data.result.id;
|
|
500
|
-
}
|
|
501
|
-
if (
|
|
502
|
-
'result' in data &&
|
|
503
|
-
data.result.type === 'stopped' &&
|
|
504
|
-
activeConnection === self
|
|
505
|
-
) {
|
|
506
|
-
req.callbacks.complete();
|
|
507
|
-
}
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
ws.onmessage = (event) => {
|
|
511
|
-
const { data } = event;
|
|
512
|
-
if (data === 'PONG') {
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
if (data === 'PING') {
|
|
516
|
-
ws.send('PONG');
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
startLazyDisconnectTimer();
|
|
520
|
-
|
|
521
|
-
const msg = JSON.parse(data) as TRPCClientIncomingMessage;
|
|
522
|
-
|
|
523
|
-
if ('method' in msg) {
|
|
524
|
-
handleIncomingRequest(msg);
|
|
525
|
-
} else {
|
|
526
|
-
handleIncomingResponse(msg);
|
|
527
|
-
}
|
|
528
|
-
if (self !== activeConnection) {
|
|
529
|
-
// when receiving a message, we close old connection that has no pending requests
|
|
530
|
-
closeIfNoPending(self);
|
|
531
|
-
}
|
|
532
|
-
};
|
|
533
|
-
|
|
534
|
-
ws.onclose = (event) => {
|
|
535
|
-
const wasOpen = self.state === 'open';
|
|
536
|
-
|
|
537
|
-
destroy();
|
|
538
|
-
onCloseOrError(new TRPCWebSocketClosedError({ cause: event }));
|
|
539
|
-
|
|
540
|
-
if (wasOpen) {
|
|
541
|
-
opts.onClose?.(event);
|
|
542
|
-
}
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
Promise.resolve(resultOf(opts.url))
|
|
547
|
-
.then(connect)
|
|
548
|
-
.catch(() => {
|
|
549
|
-
onCloseOrError(new Error('Failed to resolve url'));
|
|
550
|
-
});
|
|
551
|
-
return self;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function request(opts: {
|
|
555
|
-
op: Operation;
|
|
556
|
-
callbacks: TCallbacks;
|
|
557
|
-
lastEventId: string | undefined;
|
|
558
|
-
}): UnsubscribeFn {
|
|
559
|
-
const { op, callbacks, lastEventId } = opts;
|
|
560
|
-
const { type, input, path, id } = op;
|
|
561
|
-
const envelope: TRPCRequestMessage = {
|
|
562
|
-
id,
|
|
563
|
-
method: type,
|
|
564
|
-
params: {
|
|
565
|
-
input,
|
|
566
|
-
path,
|
|
567
|
-
lastEventId,
|
|
568
|
-
},
|
|
569
|
-
};
|
|
570
|
-
|
|
571
|
-
pendingRequests[id] = {
|
|
572
|
-
connection: null,
|
|
573
|
-
type,
|
|
574
|
-
callbacks,
|
|
575
|
-
op,
|
|
576
|
-
lastEventId,
|
|
577
|
-
};
|
|
578
|
-
|
|
579
|
-
// enqueue message
|
|
580
|
-
outgoing.push(envelope);
|
|
581
|
-
|
|
582
|
-
dispatch();
|
|
583
|
-
|
|
584
|
-
return () => {
|
|
585
|
-
const callbacks = pendingRequests[id]?.callbacks;
|
|
586
|
-
delete pendingRequests[id];
|
|
587
|
-
outgoing = outgoing.filter((msg) => msg.id !== id);
|
|
588
|
-
|
|
589
|
-
callbacks?.complete?.();
|
|
590
|
-
if (activeConnection?.state === 'open' && op.type === 'subscription') {
|
|
591
|
-
outgoing.push({
|
|
592
|
-
id,
|
|
593
|
-
method: 'subscription.stop',
|
|
594
|
-
});
|
|
595
|
-
dispatch();
|
|
596
|
-
}
|
|
597
|
-
startLazyDisconnectTimer();
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
return {
|
|
602
|
-
close: () => {
|
|
603
|
-
connectAttempt = 0;
|
|
604
|
-
|
|
605
|
-
for (const req of Object.values(pendingRequests)) {
|
|
606
|
-
if (req.type === 'subscription') {
|
|
607
|
-
req.callbacks.complete();
|
|
608
|
-
} else if (!req.connection) {
|
|
609
|
-
// close pending requests that aren't attached to a connection yet
|
|
610
|
-
req.callbacks.error(
|
|
611
|
-
TRPCClientError.from(
|
|
612
|
-
new TRPCWebSocketClosedError({
|
|
613
|
-
message: 'Closed before connection was established',
|
|
614
|
-
}),
|
|
615
|
-
),
|
|
616
|
-
);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
if (activeConnection) {
|
|
620
|
-
closeIfNoPending(activeConnection);
|
|
621
|
-
}
|
|
622
|
-
clearTimeout(connectTimer);
|
|
623
|
-
connectTimer = undefined;
|
|
624
|
-
activeConnection = null;
|
|
625
|
-
},
|
|
626
|
-
request,
|
|
627
|
-
get connection() {
|
|
628
|
-
return activeConnection;
|
|
629
|
-
},
|
|
630
|
-
/**
|
|
631
|
-
* Reconnect to the WebSocket server
|
|
632
|
-
*/
|
|
633
|
-
reconnect,
|
|
634
|
-
connectionState: connectionState,
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* @see https://trpc.io/docs/v11/client/links/wsLink
|
|
640
|
-
* @deprecated
|
|
641
|
-
* 🙋♂️ **Contributors needed** to continue supporting WebSockets!
|
|
642
|
-
* See https://github.com/trpc/trpc/issues/6109
|
|
643
|
-
*/
|
|
644
|
-
export type TRPCWebSocketClient = ReturnType<typeof createWSClient>;
|
|
645
|
-
|
|
646
|
-
/**
|
|
647
|
-
* @see https://trpc.io/docs/v11/client/links/wsLink
|
|
648
|
-
* @deprecated
|
|
649
|
-
* 🙋♂️ **Contributors needed** to continue supporting WebSockets!
|
|
650
|
-
* See https://github.com/trpc/trpc/issues/6109
|
|
651
|
-
*/
|
|
652
|
-
export type WebSocketLinkOptions<TRouter extends AnyRouter> = {
|
|
653
|
-
client: TRPCWebSocketClient;
|
|
654
|
-
} & TransformerOptions<inferClientTypes<TRouter>>;
|
|
655
|
-
class TRPCWebSocketClosedError extends Error {
|
|
656
|
-
constructor(opts?: { cause?: unknown; message?: string }) {
|
|
657
|
-
super(
|
|
658
|
-
opts?.message ?? 'WebSocket closed',
|
|
659
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
660
|
-
// @ts-ignore https://github.com/tc39/proposal-error-cause
|
|
661
|
-
{
|
|
662
|
-
cause: opts?.cause,
|
|
663
|
-
},
|
|
664
|
-
);
|
|
665
|
-
this.name = 'TRPCWebSocketClosedError';
|
|
666
|
-
Object.setPrototypeOf(this, TRPCWebSocketClosedError.prototype);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
/**
|
|
671
|
-
* @see https://trpc.io/docs/v11/client/links/wsLink
|
|
672
|
-
* @deprecated
|
|
673
|
-
* 🙋♂️ **Contributors needed** to continue supporting WebSockets!
|
|
674
|
-
* See https://github.com/trpc/trpc/issues/6109
|
|
675
|
-
*/
|
|
676
|
-
export function wsLink<TRouter extends AnyRouter>(
|
|
677
|
-
opts: WebSocketLinkOptions<TRouter>,
|
|
678
|
-
): TRPCLink<TRouter> {
|
|
679
|
-
const transformer = getTransformer(opts.transformer);
|
|
680
|
-
return () => {
|
|
681
|
-
const { client } = opts;
|
|
682
|
-
return ({ op }) => {
|
|
683
|
-
return observable((observer) => {
|
|
684
|
-
const { type, path, id, context } = op;
|
|
685
|
-
|
|
686
|
-
const input = transformer.input.serialize(op.input);
|
|
687
|
-
|
|
688
|
-
const connState =
|
|
689
|
-
type === 'subscription'
|
|
690
|
-
? client.connectionState.subscribe({
|
|
691
|
-
next(result) {
|
|
692
|
-
observer.next({
|
|
693
|
-
result,
|
|
694
|
-
context,
|
|
695
|
-
});
|
|
696
|
-
},
|
|
697
|
-
})
|
|
698
|
-
: null;
|
|
699
|
-
const unsubscribeRequest = client.request({
|
|
700
|
-
op: { type, path, input, id, context, signal: null },
|
|
701
|
-
callbacks: {
|
|
702
|
-
error(err) {
|
|
703
|
-
observer.error(err);
|
|
704
|
-
unsubscribeRequest();
|
|
705
|
-
},
|
|
706
|
-
complete() {
|
|
707
|
-
observer.complete();
|
|
708
|
-
},
|
|
709
|
-
next(event) {
|
|
710
|
-
const transformed = transformResult(event, transformer.output);
|
|
711
|
-
|
|
712
|
-
if (!transformed.ok) {
|
|
713
|
-
observer.error(TRPCClientError.from(transformed.error));
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
observer.next({
|
|
717
|
-
result: transformed.result,
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
if (op.type !== 'subscription') {
|
|
721
|
-
// if it isn't a subscription we don't care about next response
|
|
722
|
-
|
|
723
|
-
unsubscribeRequest();
|
|
724
|
-
observer.complete();
|
|
725
|
-
}
|
|
726
|
-
},
|
|
727
|
-
},
|
|
728
|
-
lastEventId: undefined,
|
|
729
|
-
});
|
|
730
|
-
return () => {
|
|
731
|
-
unsubscribeRequest();
|
|
732
|
-
connState?.unsubscribe();
|
|
733
|
-
};
|
|
734
|
-
});
|
|
735
|
-
};
|
|
736
|
-
};
|
|
737
|
-
}
|