@trpc/client 11.0.0-rc.592 → 11.0.0-rc.599
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 +61 -49
- 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 +10 -9
- package/dist/links/httpSubscriptionLink.d.ts.map +1 -1
- package/dist/links/httpSubscriptionLink.js +64 -2
- package/dist/links/httpSubscriptionLink.mjs +65 -3
- 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/internals/urlWithConnectionParams.d.ts +2 -1
- package/dist/links/internals/urlWithConnectionParams.d.ts.map +1 -1
- package/dist/links/internals/urlWithConnectionParams.js +3 -2
- package/dist/links/internals/urlWithConnectionParams.mjs +3 -2
- 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 +94 -20
- package/src/links/internals/retryLink.ts +42 -24
- package/src/links/internals/subscriptions.ts +26 -0
- package/src/links/internals/urlWithConnectionParams.ts +8 -2
- 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
package/dist/links/wsLink.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { observable } from '@trpc/server/observable';
|
|
1
|
+
import { behaviorSubject, observable } from '@trpc/server/observable';
|
|
2
2
|
import { transformResult } from '@trpc/server/unstable-core-do-not-import';
|
|
3
3
|
import { TRPCClientError } from '../TRPCClientError.mjs';
|
|
4
4
|
import { getTransformer } from '../internals/transformer.mjs';
|
|
@@ -10,7 +10,12 @@ const lazyDefaults = {
|
|
|
10
10
|
enabled: false,
|
|
11
11
|
closeMs: 0
|
|
12
12
|
};
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* @see https://trpc.io/docs/v11/client/links/wsLink
|
|
15
|
+
* @deprecated
|
|
16
|
+
* 🙋♂️ **Contributors needed** to continue supporting WebSockets!
|
|
17
|
+
* See https://github.com/trpc/trpc/issues/6109
|
|
18
|
+
*/ function createWSClient(opts) {
|
|
14
19
|
const { WebSocket: WebSocketImpl = WebSocket , retryDelayMs: retryDelayFn = exponentialBackoff , } = opts;
|
|
15
20
|
const lazyOpts = {
|
|
16
21
|
...lazyDefaults,
|
|
@@ -28,11 +33,21 @@ function createWSClient(opts) {
|
|
|
28
33
|
let connectionIndex = 0;
|
|
29
34
|
let lazyDisconnectTimer = undefined;
|
|
30
35
|
let activeConnection = lazyOpts.enabled ? null : createConnection();
|
|
36
|
+
const initState = activeConnection ? {
|
|
37
|
+
type: 'state',
|
|
38
|
+
state: 'connecting',
|
|
39
|
+
error: null
|
|
40
|
+
} : {
|
|
41
|
+
type: 'state',
|
|
42
|
+
state: 'idle',
|
|
43
|
+
error: null
|
|
44
|
+
};
|
|
45
|
+
const connectionState = behaviorSubject(initState);
|
|
31
46
|
/**
|
|
32
47
|
* tries to send the list of messages
|
|
33
48
|
*/ function dispatch() {
|
|
34
49
|
if (!activeConnection) {
|
|
35
|
-
|
|
50
|
+
reconnect(null);
|
|
36
51
|
return;
|
|
37
52
|
}
|
|
38
53
|
// using a timeout to batch messages
|
|
@@ -57,12 +72,12 @@ function createWSClient(opts) {
|
|
|
57
72
|
startLazyDisconnectTimer();
|
|
58
73
|
});
|
|
59
74
|
}
|
|
60
|
-
function tryReconnect() {
|
|
75
|
+
function tryReconnect(cause) {
|
|
61
76
|
if (!!connectTimer) {
|
|
62
77
|
return;
|
|
63
78
|
}
|
|
64
79
|
const timeout = retryDelayFn(connectAttempt++);
|
|
65
|
-
reconnectInMs(timeout);
|
|
80
|
+
reconnectInMs(timeout, cause);
|
|
66
81
|
}
|
|
67
82
|
function hasPendingRequests(conn) {
|
|
68
83
|
const requests = Object.values(pendingRequests);
|
|
@@ -71,20 +86,30 @@ function createWSClient(opts) {
|
|
|
71
86
|
}
|
|
72
87
|
return requests.some((req)=>req.connection === conn);
|
|
73
88
|
}
|
|
74
|
-
function reconnect() {
|
|
89
|
+
function reconnect(cause) {
|
|
75
90
|
if (lazyOpts.enabled && !hasPendingRequests()) {
|
|
76
|
-
// Skip reconnecting if there
|
|
91
|
+
// Skip reconnecting if there aren't pending requests and we're in lazy mode
|
|
77
92
|
return;
|
|
78
93
|
}
|
|
79
94
|
const oldConnection = activeConnection;
|
|
80
95
|
activeConnection = createConnection();
|
|
81
96
|
oldConnection && closeIfNoPending(oldConnection);
|
|
97
|
+
const currentState = connectionState.get();
|
|
98
|
+
if (currentState.state !== 'connecting') {
|
|
99
|
+
connectionState.next({
|
|
100
|
+
type: 'state',
|
|
101
|
+
state: 'connecting',
|
|
102
|
+
error: cause ? TRPCClientError.from(cause) : null
|
|
103
|
+
});
|
|
104
|
+
}
|
|
82
105
|
}
|
|
83
|
-
function reconnectInMs(ms) {
|
|
106
|
+
function reconnectInMs(ms, cause) {
|
|
84
107
|
if (connectTimer) {
|
|
85
108
|
return;
|
|
86
109
|
}
|
|
87
|
-
connectTimer = setTimeout(
|
|
110
|
+
connectTimer = setTimeout(()=>{
|
|
111
|
+
reconnect(cause);
|
|
112
|
+
}, ms);
|
|
88
113
|
}
|
|
89
114
|
function closeIfNoPending(conn) {
|
|
90
115
|
// disconnect as soon as there are are no pending requests
|
|
@@ -111,9 +136,14 @@ function createWSClient(opts) {
|
|
|
111
136
|
if (!activeConnection) {
|
|
112
137
|
return;
|
|
113
138
|
}
|
|
114
|
-
if (!hasPendingRequests(
|
|
139
|
+
if (!hasPendingRequests()) {
|
|
115
140
|
activeConnection.ws?.close();
|
|
116
141
|
activeConnection = null;
|
|
142
|
+
connectionState.next({
|
|
143
|
+
type: 'state',
|
|
144
|
+
state: 'idle',
|
|
145
|
+
error: null
|
|
146
|
+
});
|
|
117
147
|
}
|
|
118
148
|
}, lazyOpts.closeMs);
|
|
119
149
|
};
|
|
@@ -125,16 +155,27 @@ function createWSClient(opts) {
|
|
|
125
155
|
state: 'connecting'
|
|
126
156
|
};
|
|
127
157
|
clearTimeout(lazyDisconnectTimer);
|
|
128
|
-
|
|
158
|
+
function destroy() {
|
|
159
|
+
const noop = ()=>{
|
|
160
|
+
// no-op
|
|
161
|
+
};
|
|
162
|
+
const { ws } = self;
|
|
163
|
+
if (ws) {
|
|
164
|
+
ws.onclose = noop;
|
|
165
|
+
ws.onerror = noop;
|
|
166
|
+
ws.onmessage = noop;
|
|
167
|
+
ws.onopen = noop;
|
|
168
|
+
ws.close();
|
|
169
|
+
}
|
|
170
|
+
self.state = 'closed';
|
|
171
|
+
}
|
|
172
|
+
const onCloseOrError = (cause)=>{
|
|
129
173
|
clearTimeout(pingTimeout);
|
|
130
174
|
clearTimeout(pongTimeout);
|
|
131
|
-
if (self.state === 'closed') {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
175
|
self.state = 'closed';
|
|
135
176
|
if (activeConnection === self) {
|
|
136
177
|
// connection might have been replaced already
|
|
137
|
-
tryReconnect();
|
|
178
|
+
tryReconnect(cause);
|
|
138
179
|
}
|
|
139
180
|
for (const [key, req] of Object.entries(pendingRequests)){
|
|
140
181
|
if (req.connection !== self) {
|
|
@@ -147,25 +188,17 @@ function createWSClient(opts) {
|
|
|
147
188
|
} else {
|
|
148
189
|
// Queries and mutations will error if interrupted
|
|
149
190
|
delete pendingRequests[key];
|
|
150
|
-
req.callbacks.error?.(TRPCClientError.from(new TRPCWebSocketClosedError(
|
|
191
|
+
req.callbacks.error?.(TRPCClientError.from(cause ?? new TRPCWebSocketClosedError()));
|
|
151
192
|
}
|
|
152
193
|
}
|
|
153
194
|
};
|
|
154
|
-
const onClose = (code)=>{
|
|
155
|
-
const wasOpen = self.state === 'open';
|
|
156
|
-
onCloseOrError();
|
|
157
|
-
if (wasOpen) {
|
|
158
|
-
opts.onClose?.({
|
|
159
|
-
code
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
195
|
const onError = (evt)=>{
|
|
164
|
-
onCloseOrError(
|
|
196
|
+
onCloseOrError(new TRPCWebSocketClosedError({
|
|
197
|
+
cause: evt
|
|
198
|
+
}));
|
|
165
199
|
opts.onError?.(evt);
|
|
166
200
|
};
|
|
167
|
-
|
|
168
|
-
let url = await resultOf(opts.url);
|
|
201
|
+
function connect(url) {
|
|
169
202
|
if (opts.connectionParams) {
|
|
170
203
|
// append `?connectionParams=1` when connection params are used
|
|
171
204
|
const prefix = url.includes('?') ? '&' : '?';
|
|
@@ -175,7 +208,7 @@ function createWSClient(opts) {
|
|
|
175
208
|
self.ws = ws;
|
|
176
209
|
clearTimeout(connectTimer);
|
|
177
210
|
connectTimer = undefined;
|
|
178
|
-
ws.
|
|
211
|
+
ws.onopen = ()=>{
|
|
179
212
|
async function sendConnectionParams() {
|
|
180
213
|
if (!opts.connectionParams) {
|
|
181
214
|
return;
|
|
@@ -194,8 +227,11 @@ function createWSClient(opts) {
|
|
|
194
227
|
const schedulePing = ()=>{
|
|
195
228
|
const schedulePongTimeout = ()=>{
|
|
196
229
|
pongTimeout = setTimeout(()=>{
|
|
197
|
-
|
|
198
|
-
|
|
230
|
+
const wasOpen = self.state === 'open';
|
|
231
|
+
destroy();
|
|
232
|
+
if (wasOpen) {
|
|
233
|
+
opts.onClose?.();
|
|
234
|
+
}
|
|
199
235
|
}, pongTimeoutMs);
|
|
200
236
|
};
|
|
201
237
|
pingTimeout = setTimeout(()=>{
|
|
@@ -218,21 +254,32 @@ function createWSClient(opts) {
|
|
|
218
254
|
await sendConnectionParams();
|
|
219
255
|
connectAttempt = 0;
|
|
220
256
|
self.state = 'open';
|
|
257
|
+
// Update connection state
|
|
258
|
+
connectionState.next({
|
|
259
|
+
type: 'state',
|
|
260
|
+
state: 'pending',
|
|
261
|
+
error: null
|
|
262
|
+
});
|
|
221
263
|
opts.onOpen?.();
|
|
222
264
|
dispatch();
|
|
223
265
|
}).catch((cause)=>{
|
|
224
266
|
ws.close(// "Status codes in the range 3000-3999 are reserved for use by libraries, frameworks, and applications"
|
|
225
|
-
3000
|
|
226
|
-
|
|
267
|
+
3000);
|
|
268
|
+
onCloseOrError(new TRPCWebSocketClosedError({
|
|
269
|
+
message: 'Initialization error',
|
|
270
|
+
cause
|
|
271
|
+
}));
|
|
227
272
|
});
|
|
228
|
-
}
|
|
229
|
-
ws.
|
|
273
|
+
};
|
|
274
|
+
ws.onerror = onError;
|
|
230
275
|
const handleIncomingRequest = (req)=>{
|
|
231
276
|
if (self !== activeConnection) {
|
|
232
277
|
return;
|
|
233
278
|
}
|
|
234
279
|
if (req.method === 'reconnect') {
|
|
235
|
-
reconnect(
|
|
280
|
+
reconnect(new TRPCWebSocketClosedError({
|
|
281
|
+
message: 'Server requested reconnect'
|
|
282
|
+
}));
|
|
236
283
|
// notify subscribers
|
|
237
284
|
for (const pendingReq of Object.values(pendingRequests)){
|
|
238
285
|
if (pendingReq.type === 'subscription') {
|
|
@@ -263,7 +310,8 @@ function createWSClient(opts) {
|
|
|
263
310
|
req.callbacks.complete();
|
|
264
311
|
}
|
|
265
312
|
};
|
|
266
|
-
ws.
|
|
313
|
+
ws.onmessage = (event)=>{
|
|
314
|
+
const { data } = event;
|
|
267
315
|
if (data === 'PONG') {
|
|
268
316
|
return;
|
|
269
317
|
}
|
|
@@ -282,17 +330,21 @@ function createWSClient(opts) {
|
|
|
282
330
|
// when receiving a message, we close old connection that has no pending requests
|
|
283
331
|
closeIfNoPending(self);
|
|
284
332
|
}
|
|
285
|
-
}
|
|
286
|
-
ws.
|
|
333
|
+
};
|
|
334
|
+
ws.onclose = (event)=>{
|
|
287
335
|
const wasOpen = self.state === 'open';
|
|
288
|
-
|
|
336
|
+
destroy();
|
|
337
|
+
onCloseOrError(new TRPCWebSocketClosedError({
|
|
338
|
+
cause: event
|
|
339
|
+
}));
|
|
289
340
|
if (wasOpen) {
|
|
290
|
-
opts.onClose?.(
|
|
291
|
-
code
|
|
292
|
-
});
|
|
341
|
+
opts.onClose?.(event);
|
|
293
342
|
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
Promise.resolve(resultOf(opts.url)).then(connect).catch(()=>{
|
|
346
|
+
onCloseOrError(new Error('Failed to resolve url'));
|
|
347
|
+
});
|
|
296
348
|
return self;
|
|
297
349
|
}
|
|
298
350
|
function request(opts) {
|
|
@@ -340,7 +392,9 @@ function createWSClient(opts) {
|
|
|
340
392
|
req.callbacks.complete();
|
|
341
393
|
} else if (!req.connection) {
|
|
342
394
|
// close pending requests that aren't attached to a connection yet
|
|
343
|
-
req.callbacks.error(TRPCClientError.from(new
|
|
395
|
+
req.callbacks.error(TRPCClientError.from(new TRPCWebSocketClosedError({
|
|
396
|
+
message: 'Closed before connection was established'
|
|
397
|
+
})));
|
|
344
398
|
}
|
|
345
399
|
}
|
|
346
400
|
activeConnection && closeIfNoPending(activeConnection);
|
|
@@ -354,18 +408,26 @@ function createWSClient(opts) {
|
|
|
354
408
|
},
|
|
355
409
|
/**
|
|
356
410
|
* Reconnect to the WebSocket server
|
|
357
|
-
*/ reconnect
|
|
411
|
+
*/ reconnect,
|
|
412
|
+
connectionState: connectionState
|
|
358
413
|
};
|
|
359
414
|
}
|
|
360
415
|
class TRPCWebSocketClosedError extends Error {
|
|
361
|
-
constructor(
|
|
362
|
-
super(message
|
|
416
|
+
constructor(opts){
|
|
417
|
+
super(opts?.message ?? 'WebSocket closed', // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
418
|
+
// @ts-ignore https://github.com/tc39/proposal-error-cause
|
|
419
|
+
{
|
|
420
|
+
cause: opts?.cause
|
|
421
|
+
});
|
|
363
422
|
this.name = 'TRPCWebSocketClosedError';
|
|
364
423
|
Object.setPrototypeOf(this, TRPCWebSocketClosedError.prototype);
|
|
365
424
|
}
|
|
366
425
|
}
|
|
367
426
|
/**
|
|
368
427
|
* @see https://trpc.io/docs/v11/client/links/wsLink
|
|
428
|
+
* @deprecated
|
|
429
|
+
* 🙋♂️ **Contributors needed** to continue supporting WebSockets!
|
|
430
|
+
* See https://github.com/trpc/trpc/issues/6109
|
|
369
431
|
*/ function wsLink(opts) {
|
|
370
432
|
const transformer = getTransformer(opts.transformer);
|
|
371
433
|
return ()=>{
|
|
@@ -374,7 +436,15 @@ class TRPCWebSocketClosedError extends Error {
|
|
|
374
436
|
return observable((observer)=>{
|
|
375
437
|
const { type , path , id , context } = op;
|
|
376
438
|
const input = transformer.input.serialize(op.input);
|
|
377
|
-
const
|
|
439
|
+
const connState = type === 'subscription' ? client.connectionState.subscribe({
|
|
440
|
+
next (result) {
|
|
441
|
+
observer.next({
|
|
442
|
+
result,
|
|
443
|
+
context
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}) : null;
|
|
447
|
+
const unsubscribeRequest = client.request({
|
|
378
448
|
op: {
|
|
379
449
|
type,
|
|
380
450
|
path,
|
|
@@ -386,13 +456,13 @@ class TRPCWebSocketClosedError extends Error {
|
|
|
386
456
|
callbacks: {
|
|
387
457
|
error (err) {
|
|
388
458
|
observer.error(err);
|
|
389
|
-
|
|
459
|
+
unsubscribeRequest();
|
|
390
460
|
},
|
|
391
461
|
complete () {
|
|
392
462
|
observer.complete();
|
|
393
463
|
},
|
|
394
|
-
next (
|
|
395
|
-
const transformed = transformResult(
|
|
464
|
+
next (event) {
|
|
465
|
+
const transformed = transformResult(event, transformer.output);
|
|
396
466
|
if (!transformed.ok) {
|
|
397
467
|
observer.error(TRPCClientError.from(transformed.error));
|
|
398
468
|
return;
|
|
@@ -402,7 +472,7 @@ class TRPCWebSocketClosedError extends Error {
|
|
|
402
472
|
});
|
|
403
473
|
if (op.type !== 'subscription') {
|
|
404
474
|
// if it isn't a subscription we don't care about next response
|
|
405
|
-
|
|
475
|
+
unsubscribeRequest();
|
|
406
476
|
observer.complete();
|
|
407
477
|
}
|
|
408
478
|
}
|
|
@@ -410,7 +480,8 @@ class TRPCWebSocketClosedError extends Error {
|
|
|
410
480
|
lastEventId: undefined
|
|
411
481
|
});
|
|
412
482
|
return ()=>{
|
|
413
|
-
|
|
483
|
+
unsubscribeRequest();
|
|
484
|
+
connState?.unsubscribe();
|
|
414
485
|
};
|
|
415
486
|
});
|
|
416
487
|
};
|
package/dist/links.d.ts
CHANGED
package/dist/links.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAE9B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,8BAA8B,CAAC"}
|
|
1
|
+
{"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAE9B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unstable-internals.d.ts","sourceRoot":"","sources":["../src/unstable-internals.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"unstable-internals.d.ts","sourceRoot":"","sources":["../src/unstable-internals.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,iCAAiC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trpc/client",
|
|
3
|
-
"version": "11.0.0-rc.
|
|
3
|
+
"version": "11.0.0-rc.599+e16cd8d7c",
|
|
4
4
|
"description": "The tRPC client library",
|
|
5
5
|
"author": "KATT",
|
|
6
6
|
"license": "MIT",
|
|
@@ -76,10 +76,10 @@
|
|
|
76
76
|
"!**/*.test.*"
|
|
77
77
|
],
|
|
78
78
|
"peerDependencies": {
|
|
79
|
-
"@trpc/server": "11.0.0-rc.
|
|
79
|
+
"@trpc/server": "11.0.0-rc.599+e16cd8d7c"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@trpc/server": "11.0.0-rc.
|
|
82
|
+
"@trpc/server": "11.0.0-rc.599+e16cd8d7c",
|
|
83
83
|
"@types/isomorphic-fetch": "^0.0.39",
|
|
84
84
|
"@types/node": "^20.10.0",
|
|
85
85
|
"eslint": "^8.57.0",
|
|
@@ -96,5 +96,5 @@
|
|
|
96
96
|
"funding": [
|
|
97
97
|
"https://trpc.io/sponsor"
|
|
98
98
|
],
|
|
99
|
-
"gitHead": "
|
|
99
|
+
"gitHead": "e16cd8d7cee12eaf1545273858407f0947ca97fc"
|
|
100
100
|
}
|
package/src/TRPCClientError.ts
CHANGED
|
@@ -90,7 +90,7 @@ export class TRPCClientError<TRouterOrProcedure extends InferrableClientTypes>
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
public static from<TRouterOrProcedure extends InferrableClientTypes>(
|
|
93
|
-
_cause: Error | TRPCErrorResponse<any
|
|
93
|
+
_cause: Error | TRPCErrorResponse<any> | object,
|
|
94
94
|
opts: { meta?: Record<string, unknown> } = {},
|
|
95
95
|
): TRPCClientError<TRouterOrProcedure> {
|
|
96
96
|
const cause = _cause as unknown;
|
|
@@ -5,11 +5,13 @@ import type {
|
|
|
5
5
|
import { observableToPromise, share } from '@trpc/server/observable';
|
|
6
6
|
import type {
|
|
7
7
|
AnyRouter,
|
|
8
|
+
inferAsyncIterableYield,
|
|
8
9
|
InferrableClientTypes,
|
|
9
10
|
Maybe,
|
|
10
11
|
TypeError,
|
|
11
12
|
} from '@trpc/server/unstable-core-do-not-import';
|
|
12
13
|
import { createChain } from '../links/internals/createChain';
|
|
14
|
+
import type { TRPCConnectionState } from '../links/internals/subscriptions';
|
|
13
15
|
import type {
|
|
14
16
|
OperationContext,
|
|
15
17
|
OperationLink,
|
|
@@ -27,14 +29,13 @@ export interface TRPCRequestOptions {
|
|
|
27
29
|
signal?: AbortSignal;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
type inferAsyncIterableYield<T> = T extends AsyncIterable<infer U> ? U : T;
|
|
31
|
-
|
|
32
32
|
export interface TRPCSubscriptionObserver<TValue, TError> {
|
|
33
33
|
onStarted: (opts: { context: OperationContext | undefined }) => void;
|
|
34
34
|
onData: (value: inferAsyncIterableYield<TValue>) => void;
|
|
35
35
|
onError: (err: TError) => void;
|
|
36
36
|
onStopped: () => void;
|
|
37
37
|
onComplete: () => void;
|
|
38
|
+
onConnectionStateChange: (state: TRPCConnectionState<TError>) => void;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/** @internal */
|
|
@@ -139,14 +140,26 @@ export class TRPCUntypedClient<TRouter extends AnyRouter> {
|
|
|
139
140
|
});
|
|
140
141
|
return observable$.subscribe({
|
|
141
142
|
next(envelope) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
143
|
+
switch (envelope.result.type) {
|
|
144
|
+
case 'state': {
|
|
145
|
+
opts.onConnectionStateChange?.(envelope.result);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case 'started': {
|
|
149
|
+
opts.onStarted?.({
|
|
150
|
+
context: envelope.context,
|
|
151
|
+
});
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case 'stopped': {
|
|
155
|
+
opts.onStopped?.();
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case 'data':
|
|
159
|
+
case undefined: {
|
|
160
|
+
opts.onData?.(envelope.result.data);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
150
163
|
}
|
|
151
164
|
},
|
|
152
165
|
error(err) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { observable } from '@trpc/server/observable';
|
|
1
|
+
import { behaviorSubject, observable } from '@trpc/server/observable';
|
|
2
|
+
import type { TRPC_ERROR_CODE_NUMBER, TRPCErrorShape } from '@trpc/server/rpc';
|
|
3
|
+
import { TRPC_ERROR_CODES_BY_KEY } from '@trpc/server/rpc';
|
|
2
4
|
import type {
|
|
3
5
|
AnyClientTypes,
|
|
6
|
+
EventSourceLike,
|
|
4
7
|
inferClientTypes,
|
|
5
8
|
InferrableClientTypes,
|
|
6
|
-
SSEStreamConsumerOptions,
|
|
7
9
|
} from '@trpc/server/unstable-core-do-not-import';
|
|
8
10
|
import {
|
|
9
11
|
run,
|
|
@@ -11,14 +13,14 @@ import {
|
|
|
11
13
|
} from '@trpc/server/unstable-core-do-not-import';
|
|
12
14
|
import { raceAbortSignals } from '../internals/signals';
|
|
13
15
|
import { TRPCClientError } from '../TRPCClientError';
|
|
16
|
+
import type { TRPCConnectionState } from '../unstable-internals';
|
|
14
17
|
import { getTransformer, type TransformerOptions } from '../unstable-internals';
|
|
15
18
|
import { getUrl } from './internals/httpUtils';
|
|
16
|
-
import type { CallbackOrValue } from './internals/urlWithConnectionParams';
|
|
17
19
|
import {
|
|
18
20
|
resultOf,
|
|
19
21
|
type UrlOptionsWithConnectionParams,
|
|
20
22
|
} from './internals/urlWithConnectionParams';
|
|
21
|
-
import type { TRPCLink } from './types';
|
|
23
|
+
import type { Operation, TRPCLink } from './types';
|
|
22
24
|
|
|
23
25
|
async function urlWithConnectionParams(
|
|
24
26
|
opts: UrlOptionsWithConnectionParams,
|
|
@@ -35,25 +37,49 @@ async function urlWithConnectionParams(
|
|
|
35
37
|
return url;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
type HTTPSubscriptionLinkOptions<
|
|
40
|
+
type HTTPSubscriptionLinkOptions<
|
|
41
|
+
TRoot extends AnyClientTypes,
|
|
42
|
+
TEventSource extends EventSourceLike.AnyConstructor = typeof EventSource,
|
|
43
|
+
> = {
|
|
39
44
|
/**
|
|
40
|
-
* EventSource
|
|
45
|
+
* EventSource ponyfill
|
|
41
46
|
*/
|
|
42
|
-
|
|
47
|
+
EventSource?: TEventSource;
|
|
43
48
|
/**
|
|
44
|
-
*
|
|
49
|
+
* EventSource options or a callback that returns them
|
|
45
50
|
*/
|
|
46
|
-
|
|
51
|
+
eventSourceOptions?:
|
|
52
|
+
| EventSourceLike.InitDictOf<TEventSource>
|
|
53
|
+
| ((opts: {
|
|
54
|
+
op: Operation;
|
|
55
|
+
}) =>
|
|
56
|
+
| EventSourceLike.InitDictOf<TEventSource>
|
|
57
|
+
| Promise<EventSourceLike.InitDictOf<TEventSource>>);
|
|
47
58
|
} & TransformerOptions<TRoot> &
|
|
48
59
|
UrlOptionsWithConnectionParams;
|
|
49
60
|
|
|
61
|
+
/**
|
|
62
|
+
* tRPC error codes that are considered retryable
|
|
63
|
+
* With out of the box SSE, the client will reconnect when these errors are encountered
|
|
64
|
+
*/
|
|
65
|
+
const codes5xx: TRPC_ERROR_CODE_NUMBER[] = [
|
|
66
|
+
TRPC_ERROR_CODES_BY_KEY.BAD_GATEWAY,
|
|
67
|
+
TRPC_ERROR_CODES_BY_KEY.SERVICE_UNAVAILABLE,
|
|
68
|
+
TRPC_ERROR_CODES_BY_KEY.GATEWAY_TIMEOUT,
|
|
69
|
+
TRPC_ERROR_CODES_BY_KEY.INTERNAL_SERVER_ERROR,
|
|
70
|
+
];
|
|
71
|
+
|
|
50
72
|
/**
|
|
51
73
|
* @see https://trpc.io/docs/client/links/httpSubscriptionLink
|
|
52
74
|
*/
|
|
53
75
|
export function unstable_httpSubscriptionLink<
|
|
54
76
|
TInferrable extends InferrableClientTypes,
|
|
77
|
+
TEventSource extends EventSourceLike.AnyConstructor,
|
|
55
78
|
>(
|
|
56
|
-
opts: HTTPSubscriptionLinkOptions<
|
|
79
|
+
opts: HTTPSubscriptionLinkOptions<
|
|
80
|
+
inferClientTypes<TInferrable>,
|
|
81
|
+
TEventSource
|
|
82
|
+
>,
|
|
57
83
|
): TRPCLink<TInferrable> {
|
|
58
84
|
const transformer = getTransformer(opts.transformer);
|
|
59
85
|
|
|
@@ -69,12 +95,14 @@ export function unstable_httpSubscriptionLink<
|
|
|
69
95
|
|
|
70
96
|
const ac = new AbortController();
|
|
71
97
|
const signal = raceAbortSignals(op.signal, ac.signal);
|
|
72
|
-
const eventSourceStream = sseStreamConsumer<
|
|
73
|
-
|
|
98
|
+
const eventSourceStream = sseStreamConsumer<{
|
|
99
|
+
EventSource: TEventSource;
|
|
100
|
+
data: Partial<{
|
|
74
101
|
id?: string;
|
|
75
102
|
data: unknown;
|
|
76
|
-
}
|
|
77
|
-
|
|
103
|
+
}>;
|
|
104
|
+
error: TRPCErrorShape;
|
|
105
|
+
}>({
|
|
78
106
|
url: async () =>
|
|
79
107
|
getUrl({
|
|
80
108
|
transformer,
|
|
@@ -84,12 +112,29 @@ export function unstable_httpSubscriptionLink<
|
|
|
84
112
|
type,
|
|
85
113
|
signal: null,
|
|
86
114
|
}),
|
|
87
|
-
init: () => resultOf(opts.eventSourceOptions),
|
|
115
|
+
init: () => resultOf(opts.eventSourceOptions, { op }),
|
|
88
116
|
signal,
|
|
89
117
|
deserialize: transformer.output.deserialize,
|
|
90
|
-
|
|
118
|
+
EventSource:
|
|
119
|
+
opts.EventSource ??
|
|
120
|
+
(globalThis.EventSource as never as TEventSource),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const connectionState = behaviorSubject<
|
|
124
|
+
TRPCConnectionState<TRPCClientError<any>>
|
|
125
|
+
>({
|
|
126
|
+
type: 'state',
|
|
127
|
+
state: 'connecting',
|
|
128
|
+
error: null,
|
|
91
129
|
});
|
|
92
130
|
|
|
131
|
+
const connectionSub = connectionState.subscribe({
|
|
132
|
+
next(state) {
|
|
133
|
+
observer.next({
|
|
134
|
+
result: state,
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
});
|
|
93
138
|
run(async () => {
|
|
94
139
|
for await (const chunk of eventSourceStream) {
|
|
95
140
|
switch (chunk.type) {
|
|
@@ -117,14 +162,42 @@ export function unstable_httpSubscriptionLink<
|
|
|
117
162
|
eventSource: chunk.eventSource,
|
|
118
163
|
},
|
|
119
164
|
});
|
|
165
|
+
connectionState.next({
|
|
166
|
+
type: 'state',
|
|
167
|
+
state: 'pending',
|
|
168
|
+
error: null,
|
|
169
|
+
});
|
|
120
170
|
break;
|
|
121
171
|
}
|
|
122
|
-
case 'error': {
|
|
123
|
-
|
|
124
|
-
|
|
172
|
+
case 'serialized-error': {
|
|
173
|
+
const error = TRPCClientError.from({ error: chunk.error });
|
|
174
|
+
|
|
175
|
+
if (codes5xx.includes(chunk.error.code)) {
|
|
176
|
+
//
|
|
177
|
+
connectionState.next({
|
|
178
|
+
type: 'state',
|
|
179
|
+
state: 'connecting',
|
|
180
|
+
error,
|
|
181
|
+
});
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
//
|
|
185
|
+
// non-retryable error, cancel the subscription
|
|
186
|
+
throw error;
|
|
125
187
|
}
|
|
126
188
|
case 'connecting': {
|
|
127
|
-
|
|
189
|
+
const lastState = connectionState.get();
|
|
190
|
+
|
|
191
|
+
const error = chunk.event && TRPCClientError.from(chunk.event);
|
|
192
|
+
if (!error && lastState.state === 'connecting') {
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
connectionState.next({
|
|
197
|
+
type: 'state',
|
|
198
|
+
state: 'connecting',
|
|
199
|
+
error,
|
|
200
|
+
});
|
|
128
201
|
break;
|
|
129
202
|
}
|
|
130
203
|
}
|
|
@@ -143,6 +216,7 @@ export function unstable_httpSubscriptionLink<
|
|
|
143
216
|
return () => {
|
|
144
217
|
observer.complete();
|
|
145
218
|
ac.abort();
|
|
219
|
+
connectionSub.unsubscribe();
|
|
146
220
|
};
|
|
147
221
|
});
|
|
148
222
|
};
|