@trpc/client 11.0.0-rc.560 → 11.0.0-rc.563

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.
@@ -5,6 +5,7 @@ import type { AnyRootTypes } from '@trpc/server/unstable-core-do-not-import';
5
5
  import { jsonlStreamConsumer } from '@trpc/server/unstable-core-do-not-import';
6
6
  import type { BatchLoader } from '../internals/dataLoader';
7
7
  import { dataLoader } from '../internals/dataLoader';
8
+ import { allAbortSignals, raceAbortSignals } from '../internals/signals';
8
9
  import type { NonEmptyArray } from '../internals/types';
9
10
  import { TRPCClientError } from '../TRPCClientError';
10
11
  import type { HTTPBatchLinkOptions } from './HTTPBatchLinkOptions';
@@ -13,7 +14,6 @@ import {
13
14
  fetchHTTPResponse,
14
15
  getBody,
15
16
  getUrl,
16
- mergeAbortSignals,
17
17
  resolveHTTPLinkOptions,
18
18
  } from './internals/httpUtils';
19
19
  import type { Operation, TRPCLink } from './types';
@@ -67,11 +67,14 @@ export function unstable_httpBatchStreamLink<TRouter extends AnyRouter>(
67
67
  const path = batchOps.map((op) => op.path).join(',');
68
68
  const inputs = batchOps.map((op) => op.input);
69
69
 
70
- const ac = mergeAbortSignals(batchOps);
70
+ const batchSignals = allAbortSignals(
71
+ ...batchOps.map((op) => op.signal),
72
+ );
73
+ const abortController = new AbortController();
71
74
 
72
75
  const responsePromise = fetchHTTPResponse({
73
76
  ...resolvedOpts,
74
- signal: ac.signal,
77
+ signal: raceAbortSignals(batchSignals, abortController.signal),
75
78
  type,
76
79
  contentTypeHeader: 'application/json',
77
80
  trpcAcceptHeader: 'application/jsonl',
@@ -106,7 +109,7 @@ export function unstable_httpBatchStreamLink<TRouter extends AnyRouter>(
106
109
  error,
107
110
  });
108
111
  },
109
- abortController: ac,
112
+ abortController,
110
113
  });
111
114
  const promises = Object.keys(batchOps).map(
112
115
  async (key): Promise<HTTPResult> => {
@@ -3,11 +3,13 @@ import type {
3
3
  AnyClientTypes,
4
4
  inferClientTypes,
5
5
  InferrableClientTypes,
6
+ SSEStreamConsumerOptions,
6
7
  } from '@trpc/server/unstable-core-do-not-import';
7
8
  import {
8
9
  run,
9
10
  sseStreamConsumer,
10
11
  } from '@trpc/server/unstable-core-do-not-import';
12
+ import { raceAbortSignals } from '../internals/signals';
11
13
  import { TRPCClientError } from '../TRPCClientError';
12
14
  import { getTransformer, type TransformerOptions } from '../unstable-internals';
13
15
  import { getUrl } from './internals/httpUtils';
@@ -33,31 +35,15 @@ async function urlWithConnectionParams(
33
35
  return url;
34
36
  }
35
37
 
36
- type RecreateOnErrorOpt =
37
- | {
38
- type: 'raw';
39
- event: Event;
40
- }
41
- | {
42
- type: 'http-error';
43
- status: number;
44
- event: Event;
45
- };
46
-
47
38
  type HTTPSubscriptionLinkOptions<TRoot extends AnyClientTypes> = {
48
39
  /**
49
40
  * EventSource options or a callback that returns them
50
41
  */
51
42
  eventSourceOptions?: CallbackOrValue<EventSourceInit>;
52
43
  /**
53
- * For a given error, should we reinitialize the underlying EventSource?
54
- *
55
- * This is useful where a long running subscription might be interrupted by a recoverable network error,
56
- * but the existing authorization in a header or URI has expired in the mean-time
44
+ * @see https://trpc.io/docs/client/links/httpSubscriptionLink#updatingConfig
57
45
  */
58
- shouldRecreateOnError?: (
59
- opt: RecreateOnErrorOpt,
60
- ) => boolean | Promise<boolean>;
46
+ experimental_shouldRecreateOnError?: SSEStreamConsumerOptions['shouldRecreateOnError'];
61
47
  } & TransformerOptions<TRoot> &
62
48
  UrlOptionsWithConnectionParams;
63
49
 
@@ -75,102 +61,73 @@ export function unstable_httpSubscriptionLink<
75
61
  return ({ op }) => {
76
62
  return observable((observer) => {
77
63
  const { type, path, input } = op;
64
+
78
65
  /* istanbul ignore if -- @preserve */
79
66
  if (type !== 'subscription') {
80
67
  throw new Error('httpSubscriptionLink only supports subscriptions');
81
68
  }
82
69
 
83
- let eventSource: EventSourceWrapper | null = null;
84
- let unsubscribed = false;
70
+ const ac = new AbortController();
71
+ const signal = raceAbortSignals(op.signal, ac.signal);
72
+ const eventSourceStream = sseStreamConsumer<
73
+ Partial<{
74
+ id?: string;
75
+ data: unknown;
76
+ }>
77
+ >({
78
+ url: async () =>
79
+ getUrl({
80
+ transformer,
81
+ url: await urlWithConnectionParams(opts),
82
+ input,
83
+ path,
84
+ type,
85
+ signal: null,
86
+ }),
87
+ init: () => resultOf(opts.eventSourceOptions),
88
+ signal,
89
+ deserialize: transformer.output.deserialize,
90
+ shouldRecreateOnError: opts.experimental_shouldRecreateOnError,
91
+ });
85
92
 
86
93
  run(async () => {
87
- const url = getUrl({
88
- transformer,
89
- url: await urlWithConnectionParams(opts),
90
- input,
91
- path,
92
- type,
93
- signal: null,
94
- });
95
-
96
- const eventSourceOptions = await resultOf(opts.eventSourceOptions);
97
- /* istanbul ignore if -- @preserve */
98
- if (unsubscribed) {
99
- // already unsubscribed - rare race condition
100
- return;
101
- }
102
-
103
- eventSource = new EventSourceWrapper(url, eventSourceOptions);
104
-
105
- const onStarted = () => {
106
- observer.next({
107
- result: {
108
- type: 'started',
109
- },
110
- context: {
111
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
112
- eventSource: eventSource!.getEventSource(),
113
- },
114
- });
115
- };
116
- eventSource.addEventListener('open', onStarted, { once: true });
117
-
118
- const iterable = sseStreamConsumer<
119
- Partial<{
120
- id?: string;
121
- data: unknown;
122
- }>
123
- >({
124
- from: eventSource,
125
- deserialize: transformer.output.deserialize,
126
- tryHandleError: async (ev) => {
127
- if (
128
- typeof opts.shouldRecreateOnError !== 'function' ||
129
- !eventSource
130
- ) {
131
- return false;
94
+ for await (const chunk of eventSourceStream) {
95
+ switch (chunk.type) {
96
+ case 'data':
97
+ const chunkData = chunk.data;
98
+
99
+ // if the `tracked()`-helper is used, we always have an `id` field
100
+ const data = 'id' in chunkData ? chunkData : chunkData.data;
101
+
102
+ observer.next({
103
+ result: {
104
+ data,
105
+ },
106
+ context: {
107
+ eventSource: chunk.eventSource,
108
+ },
109
+ });
110
+ break;
111
+ case 'opened': {
112
+ observer.next({
113
+ result: {
114
+ type: 'started',
115
+ },
116
+ context: {
117
+ eventSource: chunk.eventSource,
118
+ },
119
+ });
120
+ break;
132
121
  }
133
-
134
- const recreateOnErrorOpts = createRecreateOnErrorOpts(ev);
135
-
136
- const shouldRestart = await opts.shouldRecreateOnError(
137
- recreateOnErrorOpts,
138
- );
139
-
140
- if (!shouldRestart) {
141
- return false;
122
+ case 'error': {
123
+ // TODO: handle in https://github.com/trpc/trpc/issues/5871
124
+ break;
125
+ }
126
+ case 'connecting': {
127
+ // TODO: handle in https://github.com/trpc/trpc/issues/5871
128
+ break;
142
129
  }
143
-
144
- eventSource.restart(
145
- getUrl({
146
- transformer,
147
- url: await urlWithConnectionParams(opts),
148
- input,
149
- path,
150
- type,
151
- signal: null,
152
- }),
153
- await resultOf(opts.eventSourceOptions),
154
- );
155
-
156
- return true;
157
- },
158
- });
159
-
160
- for await (const chunk of iterable) {
161
- if (!chunk.ok) {
162
- // TODO: handle in https://github.com/trpc/trpc/issues/5871
163
- continue;
164
130
  }
165
- const chunkData = chunk.data;
166
-
167
- // if the `tracked()`-helper is used, we always have an `id` field
168
- const data = 'id' in chunkData ? chunkData : chunkData.data;
169
- observer.next({
170
- result: {
171
- data,
172
- },
173
- });
174
131
  }
175
132
 
176
133
  observer.next({
@@ -185,104 +142,9 @@ export function unstable_httpSubscriptionLink<
185
142
 
186
143
  return () => {
187
144
  observer.complete();
188
- eventSource?.close();
189
- unsubscribed = true;
145
+ ac.abort();
190
146
  };
191
147
  });
192
148
  };
193
149
  };
194
150
  }
195
-
196
- function createRecreateOnErrorOpts(ev: Event): RecreateOnErrorOpt {
197
- if ('status' in ev && typeof ev.status === 'number') {
198
- return {
199
- type: 'http-error',
200
- status: ev.status,
201
- event: ev,
202
- };
203
- }
204
-
205
- return {
206
- type: 'raw',
207
- event: ev,
208
- };
209
- }
210
-
211
- /**
212
- * We wrap EventSource so that is can be reinitialized with new options
213
- */
214
- class EventSourceWrapper implements Partial<EventSource> {
215
- private es: EventSource;
216
-
217
- private listeners: Partial<
218
- Record<
219
- keyof EventSourceEventMap,
220
- Parameters<EventSource['addEventListener']>[]
221
- >
222
- > = {};
223
- private *getAllEventListeners() {
224
- for (const _type in this.listeners) {
225
- const type = _type as keyof typeof this.listeners;
226
- for (const listener of this.listeners[type] ?? []) {
227
- yield listener;
228
- }
229
- }
230
- }
231
-
232
- constructor(url: string, options: EventSourceInit | undefined) {
233
- this.es = new EventSource(url, options);
234
- }
235
-
236
- restart(url: string, options: EventSourceInit | undefined) {
237
- for (const [type, callback, options] of this.getAllEventListeners()) {
238
- this.es.removeEventListener(type, callback, options);
239
- }
240
-
241
- this.es.close();
242
- this.es = new EventSource(url, options);
243
-
244
- for (const [type, callback, options] of this.getAllEventListeners()) {
245
- this.es.addEventListener(type, callback, options);
246
- }
247
- }
248
-
249
- close() {
250
- this.listeners = {};
251
- this.es.close();
252
- }
253
-
254
- getEventSource() {
255
- return this.es;
256
- }
257
-
258
- get readyState() {
259
- return this.es.readyState;
260
- }
261
-
262
- addEventListener<TEvent extends keyof EventSourceEventMap>(
263
- type: TEvent,
264
- listener: (this: EventSource, ev: EventSourceEventMap[TEvent]) => any,
265
- options?: boolean | AddEventListenerOptions,
266
- ) {
267
- this.listeners[type] ??= [];
268
- this.listeners[type].push([type, listener as any, options]);
269
-
270
- this.es.addEventListener(type, listener, options);
271
- }
272
-
273
- removeEventListener<TEvent extends keyof EventSourceEventMap>(
274
- type: TEvent,
275
- listener: (this: EventSource, ev: EventSourceEventMap[TEvent]) => any,
276
- options?: boolean | EventListenerOptions,
277
- ) {
278
- this.listeners[type] ??= [];
279
-
280
- const indexToRemove = this.listeners[type]?.findIndex(
281
- ([_type, thisListener]) => thisListener === listener,
282
- );
283
- if (typeof indexToRemove === 'number' && indexToRemove >= 0) {
284
- this.listeners[type].splice(indexToRemove, 1);
285
- }
286
- this.es.removeEventListener(type, listener, options);
287
- }
288
- }
@@ -240,43 +240,3 @@ export async function httpRequest(
240
240
  meta,
241
241
  };
242
242
  }
243
-
244
- /**
245
- * Merges multiple abort signals into a single one
246
- * - When all signals have been aborted, the merged signal will be aborted
247
- */
248
- export function mergeAbortSignals(
249
- opts: {
250
- signal: Maybe<AbortSignal>;
251
- }[],
252
- ): AbortController {
253
- const ac = new AbortController();
254
-
255
- if (opts.some((o) => !o.signal)) {
256
- return ac;
257
- }
258
-
259
- const count = opts.length;
260
-
261
- let abortedCount = 0;
262
-
263
- const onAbort = () => {
264
- if (++abortedCount === count) {
265
- ac.abort();
266
- }
267
- };
268
-
269
- for (const o of opts) {
270
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
271
- const signal = o.signal!;
272
- if (signal.aborted) {
273
- onAbort();
274
- } else {
275
- signal.addEventListener('abort', onAbort, {
276
- once: true,
277
- });
278
- }
279
- }
280
-
281
- return ac;
282
- }