@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.
Files changed (49) hide show
  1. package/dist/TRPCClientError.d.ts +1 -1
  2. package/dist/TRPCClientError.d.ts.map +1 -1
  3. package/dist/bundle-analysis.json +61 -49
  4. package/dist/index.js +2 -0
  5. package/dist/index.mjs +1 -0
  6. package/dist/internals/TRPCUntypedClient.d.ts +3 -3
  7. package/dist/internals/TRPCUntypedClient.d.ts.map +1 -1
  8. package/dist/internals/TRPCUntypedClient.js +24 -8
  9. package/dist/internals/TRPCUntypedClient.mjs +24 -8
  10. package/dist/links/httpSubscriptionLink.d.ts +10 -9
  11. package/dist/links/httpSubscriptionLink.d.ts.map +1 -1
  12. package/dist/links/httpSubscriptionLink.js +64 -2
  13. package/dist/links/httpSubscriptionLink.mjs +65 -3
  14. package/dist/links/internals/retryLink.d.ts +26 -6
  15. package/dist/links/internals/retryLink.d.ts.map +1 -1
  16. package/dist/links/internals/retryLink.js +43 -0
  17. package/dist/links/internals/retryLink.mjs +41 -0
  18. package/dist/links/internals/subscriptions.d.ts +20 -0
  19. package/dist/links/internals/subscriptions.d.ts.map +1 -0
  20. package/dist/links/internals/urlWithConnectionParams.d.ts +2 -1
  21. package/dist/links/internals/urlWithConnectionParams.d.ts.map +1 -1
  22. package/dist/links/internals/urlWithConnectionParams.js +3 -2
  23. package/dist/links/internals/urlWithConnectionParams.mjs +3 -2
  24. package/dist/links/loggerLink.d.ts +4 -4
  25. package/dist/links/loggerLink.d.ts.map +1 -1
  26. package/dist/links/loggerLink.js +1 -1
  27. package/dist/links/loggerLink.mjs +1 -1
  28. package/dist/links/types.d.ts +5 -4
  29. package/dist/links/types.d.ts.map +1 -1
  30. package/dist/links/wsLink.d.ts +24 -1
  31. package/dist/links/wsLink.d.ts.map +1 -1
  32. package/dist/links/wsLink.js +125 -54
  33. package/dist/links/wsLink.mjs +126 -55
  34. package/dist/links.d.ts +1 -0
  35. package/dist/links.d.ts.map +1 -1
  36. package/dist/unstable-internals.d.ts +1 -0
  37. package/dist/unstable-internals.d.ts.map +1 -1
  38. package/package.json +4 -4
  39. package/src/TRPCClientError.ts +1 -1
  40. package/src/internals/TRPCUntypedClient.ts +23 -10
  41. package/src/links/httpSubscriptionLink.ts +94 -20
  42. package/src/links/internals/retryLink.ts +42 -24
  43. package/src/links/internals/subscriptions.ts +26 -0
  44. package/src/links/internals/urlWithConnectionParams.ts +8 -2
  45. package/src/links/loggerLink.ts +16 -6
  46. package/src/links/types.ts +12 -4
  47. package/src/links/wsLink.ts +163 -56
  48. package/src/links.ts +1 -1
  49. package/src/unstable-internals.ts +1 -0
@@ -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
- function createWSClient(opts) {
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
- activeConnection = createConnection();
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 are pending requests and we're in lazy mode
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(reconnect, ms);
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(activeConnection)) {
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
- const onCloseOrError = ()=>{
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('WebSocket closed prematurely')));
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
- run(async ()=>{
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.addEventListener('open', ()=>{
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
- ws.close(3001);
198
- onClose(3001);
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, cause);
226
- onError();
267
+ 3000);
268
+ onCloseOrError(new TRPCWebSocketClosedError({
269
+ message: 'Initialization error',
270
+ cause
271
+ }));
227
272
  });
228
- });
229
- ws.addEventListener('error', onError);
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.addEventListener('message', ({ data })=>{
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.addEventListener('close', ({ code })=>{
333
+ };
334
+ ws.onclose = (event)=>{
287
335
  const wasOpen = self.state === 'open';
288
- onCloseOrError();
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
- }).catch(onError);
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 Error('Closed before connection was established')));
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(message){
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 unsub = client.request({
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
- unsub();
459
+ unsubscribeRequest();
390
460
  },
391
461
  complete () {
392
462
  observer.complete();
393
463
  },
394
- next (message) {
395
- const transformed = transformResult(message, transformer.output);
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
- unsub();
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
- unsub();
483
+ unsubscribeRequest();
484
+ connState?.unsubscribe();
414
485
  };
415
486
  });
416
487
  };
package/dist/links.d.ts CHANGED
@@ -7,4 +7,5 @@ export * from './links/loggerLink';
7
7
  export * from './links/splitLink';
8
8
  export * from './links/wsLink';
9
9
  export * from './links/httpSubscriptionLink';
10
+ export * from './links/internals/retryLink';
10
11
  //# sourceMappingURL=links.d.ts.map
@@ -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,2 +1,3 @@
1
1
  export * from './internals/transformer';
2
+ export * from './links/internals/subscriptions';
2
3
  //# sourceMappingURL=unstable-internals.d.ts.map
@@ -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.592+4744c0612",
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.592+4744c0612"
79
+ "@trpc/server": "11.0.0-rc.599+e16cd8d7c"
80
80
  },
81
81
  "devDependencies": {
82
- "@trpc/server": "11.0.0-rc.592+4744c0612",
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": "4744c0612c2e6dcf6d9fd3a30fd57621962ac439"
99
+ "gitHead": "e16cd8d7cee12eaf1545273858407f0947ca97fc"
100
100
  }
@@ -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
- if (envelope.result.type === 'started') {
143
- opts.onStarted?.({
144
- context: envelope.context,
145
- });
146
- } else if (envelope.result.type === 'stopped') {
147
- opts.onStopped?.();
148
- } else {
149
- opts.onData?.(envelope.result.data);
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<TRoot extends AnyClientTypes> = {
40
+ type HTTPSubscriptionLinkOptions<
41
+ TRoot extends AnyClientTypes,
42
+ TEventSource extends EventSourceLike.AnyConstructor = typeof EventSource,
43
+ > = {
39
44
  /**
40
- * EventSource options or a callback that returns them
45
+ * EventSource ponyfill
41
46
  */
42
- eventSourceOptions?: CallbackOrValue<EventSourceInit>;
47
+ EventSource?: TEventSource;
43
48
  /**
44
- * @see https://trpc.io/docs/client/links/httpSubscriptionLink#updatingConfig
49
+ * EventSource options or a callback that returns them
45
50
  */
46
- experimental_shouldRecreateOnError?: SSEStreamConsumerOptions['shouldRecreateOnError'];
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<inferClientTypes<TInferrable>>,
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
- Partial<{
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
- shouldRecreateOnError: opts.experimental_shouldRecreateOnError,
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
- // TODO: handle in https://github.com/trpc/trpc/issues/5871
124
- break;
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
- // TODO: handle in https://github.com/trpc/trpc/issues/5871
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
  };