@trpc/server 11.0.0-rc.632 → 11.0.0-rc.633

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 (30) hide show
  1. package/dist/adapters/node-http/writeResponse.d.ts.map +1 -1
  2. package/dist/adapters/node-http/writeResponse.js +0 -5
  3. package/dist/adapters/node-http/writeResponse.mjs +0 -5
  4. package/dist/adapters/ws.js +1 -1
  5. package/dist/adapters/ws.mjs +1 -1
  6. package/dist/bundle-analysis.json +82 -82
  7. package/dist/index.js +1 -1
  8. package/dist/index.mjs +1 -1
  9. package/dist/unstable-core-do-not-import/http/resolveResponse.d.ts.map +1 -1
  10. package/dist/unstable-core-do-not-import/http/resolveResponse.js +3 -3
  11. package/dist/unstable-core-do-not-import/http/resolveResponse.mjs +3 -3
  12. package/dist/unstable-core-do-not-import/initTRPC.js +2 -2
  13. package/dist/unstable-core-do-not-import/initTRPC.mjs +2 -2
  14. package/dist/unstable-core-do-not-import/rootConfig.d.ts +14 -14
  15. package/dist/unstable-core-do-not-import/rootConfig.d.ts.map +1 -1
  16. package/dist/unstable-core-do-not-import/rpc/envelopes.d.ts +7 -10
  17. package/dist/unstable-core-do-not-import/rpc/envelopes.d.ts.map +1 -1
  18. package/dist/unstable-core-do-not-import/stream/sse.d.ts +11 -1
  19. package/dist/unstable-core-do-not-import/stream/sse.d.ts.map +1 -1
  20. package/dist/unstable-core-do-not-import/stream/sse.js +129 -65
  21. package/dist/unstable-core-do-not-import/stream/sse.mjs +129 -65
  22. package/dist/unstable-core-do-not-import/transformer.d.ts +1 -4
  23. package/dist/unstable-core-do-not-import/transformer.d.ts.map +1 -1
  24. package/package.json +2 -2
  25. package/src/adapters/node-http/writeResponse.ts +0 -5
  26. package/src/unstable-core-do-not-import/http/resolveResponse.ts +3 -4
  27. package/src/unstable-core-do-not-import/initTRPC.ts +1 -1
  28. package/src/unstable-core-do-not-import/rootConfig.ts +17 -17
  29. package/src/unstable-core-do-not-import/rpc/envelopes.ts +7 -12
  30. package/src/unstable-core-do-not-import/stream/sse.ts +155 -67
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trpc/server",
3
- "version": "11.0.0-rc.632+3df8b21be",
3
+ "version": "11.0.0-rc.633+55512768a",
4
4
  "description": "The tRPC server library",
5
5
  "author": "KATT",
6
6
  "license": "MIT",
@@ -156,5 +156,5 @@
156
156
  "next": "*",
157
157
  "ws": "*"
158
158
  },
159
- "gitHead": "3df8b21beb4925cf73194fc776d1a88603a3c420"
159
+ "gitHead": "55512768ac9e57c472c9b03d8e39f606a8c1e1c6"
160
160
  }
@@ -42,11 +42,6 @@ export async function writeResponseBody(opts: {
42
42
  await writeResponseBodyChunk(res, chunk);
43
43
  res.flush?.();
44
44
  },
45
- abort() {
46
- if (!res.headersSent) {
47
- res.statusCode = 500;
48
- }
49
- },
50
45
  });
51
46
 
52
47
  await opts.body.pipeTo(writableStream, {
@@ -300,9 +300,8 @@ export async function resolveResponse<TRouter extends AnyRouter>(
300
300
  const isStreamCall = req.headers.get('trpc-accept') === 'application/jsonl';
301
301
 
302
302
  const experimentalIterablesAndDeferreds =
303
- router._def._config.experimental?.iterablesAndDeferreds ?? true;
304
- const experimentalSSE =
305
- router._def._config.experimental?.sseSubscriptions?.enabled ?? true;
303
+ config.iterablesAndDeferreds ?? true;
304
+ const experimentalSSE = config.sse?.enabled ?? true;
306
305
  try {
307
306
  const [infoError, info] = infoTuple;
308
307
  if (infoError) {
@@ -465,7 +464,7 @@ export async function resolveResponse<TRouter extends AnyRouter>(
465
464
  });
466
465
 
467
466
  const stream = sseStreamProducer({
468
- ...config.experimental?.sseSubscriptions,
467
+ ...config.sse,
469
468
  data: iterable,
470
469
  abortCtrl: result?.abortCtrl ?? new AbortController(),
471
470
  serialize: (v) => config.transformer.output.serialize(v),
@@ -76,6 +76,7 @@ class TRPCBuilder<TContext extends object, TMeta extends object> {
76
76
  }>;
77
77
 
78
78
  const config: RootConfig<$Root> = {
79
+ ...opts,
79
80
  transformer: getDataTransformer(opts?.transformer ?? defaultTransformer),
80
81
  isDev:
81
82
  opts?.isDev ??
@@ -89,7 +90,6 @@ class TRPCBuilder<TContext extends object, TMeta extends object> {
89
90
  * @internal
90
91
  */
91
92
  $types: null as any,
92
- experimental: opts?.experimental ?? {},
93
93
  };
94
94
 
95
95
  {
@@ -65,26 +65,26 @@ export interface RootConfig<TTypes extends RootTypes> {
65
65
 
66
66
  defaultMeta?: TTypes['meta'] extends object ? TTypes['meta'] : never;
67
67
 
68
- experimental?: {
68
+ /**
69
+ * Enable support for returning async iterables and returning deferred promises when using `httpBatchStreamLink`
70
+ * @default true
71
+ */
72
+ iterablesAndDeferreds?: boolean;
73
+ /**
74
+ * Options for server-sent events (SSE) subscriptions
75
+ * @see https://trpc.io/docs/client/links/httpSubscriptionLink
76
+ */
77
+ sse?: {
69
78
  /**
70
- * Enable support for returning async iterables and returning deferred promises when using `httpBatchStreamLink`
79
+ * Enable server-sent events (SSE) subscriptions
71
80
  * @default true
72
81
  */
73
- iterablesAndDeferreds?: boolean;
74
- /**
75
- * Enable support for server-sent events (SSE) subscriptions
76
- */
77
- sseSubscriptions?: {
78
- /**
79
- * Enable server-sent events (SSE) subscriptions
80
- * @default true
81
- */
82
- enabled?: boolean;
83
- } & Pick<
84
- SSEStreamProducerOptions,
85
- 'ping' | 'emitAndEndImmediately' | 'maxDurationMs'
86
- >;
87
- };
82
+ enabled?: boolean;
83
+ } & Pick<
84
+ SSEStreamProducerOptions,
85
+ 'ping' | 'emitAndEndImmediately' | 'maxDurationMs'
86
+ >;
87
+ experimental?: {};
88
88
  }
89
89
 
90
90
  /**
@@ -63,14 +63,15 @@ export interface TRPCRequest
63
63
 
64
64
  export interface TRPCResult<TData = unknown> {
65
65
  data: TData;
66
+ type?: 'data';
67
+ /**
68
+ * The id of the message to keep track of in case of a reconnect
69
+ */
70
+ id?: string;
66
71
  }
67
72
 
68
73
  export interface TRPCSuccessResponse<TData>
69
- extends JSONRPC2.ResultResponse<
70
- TRPCResult<TData> & {
71
- type?: 'data';
72
- }
73
- > {}
74
+ extends JSONRPC2.ResultResponse<TRPCResult<TData>> {}
74
75
 
75
76
  export interface TRPCErrorResponse<
76
77
  TError extends TRPCErrorShape = TRPCErrorShape,
@@ -111,13 +112,7 @@ export interface TRPCResultMessage<TData>
111
112
  extends JSONRPC2.ResultResponse<
112
113
  | { type: 'started'; data?: never }
113
114
  | { type: 'stopped'; data?: never }
114
- | (TRPCResult<TData> & {
115
- type: 'data';
116
- /**
117
- * The id of the message to keep track of in case of a reconnect
118
- */
119
- id?: string;
120
- })
115
+ | TRPCResult<TData>
121
116
  > {}
122
117
 
123
118
  export type TRPCResponseMessage<
@@ -1,3 +1,4 @@
1
+ import { Unpromise } from '../../vendor/unpromise';
1
2
  import { getTRPCErrorFromUnknown } from '../error/TRPCError';
2
3
  import { isAbortError } from '../http/isAbortError';
3
4
  import type { MaybePromise } from '../types';
@@ -49,6 +50,7 @@ export interface SSEStreamProducerOptions<TValue = unknown> {
49
50
  formatError?: (opts: { error: unknown }) => unknown;
50
51
  }
51
52
 
53
+ const PING_EVENT = 'ping';
52
54
  const SERIALIZED_ERROR_EVENT = 'serialized-error';
53
55
 
54
56
  type SSEvent = Partial<{
@@ -110,7 +112,7 @@ export function sseStreamProducer<TValue = unknown>(
110
112
 
111
113
  for await (value of iterable) {
112
114
  if (value === PING_SYM) {
113
- stream.controller.enqueue({ comment: 'ping' });
115
+ stream.controller.enqueue({ event: PING_EVENT, data: '' });
114
116
  continue;
115
117
  }
116
118
 
@@ -199,12 +201,23 @@ interface ConsumerStreamResultConnecting<TConfig extends ConsumerConfig>
199
201
  type: 'connecting';
200
202
  event: EventSourceLike.EventOf<TConfig['EventSource']> | null;
201
203
  }
204
+ interface ConsumerStreamResultTimeout<TConfig extends ConsumerConfig>
205
+ extends ConsumerStreamResultBase<TConfig> {
206
+ type: 'timeout';
207
+ }
208
+
209
+ interface ConsumerStreamResultPing<TConfig extends ConsumerConfig>
210
+ extends ConsumerStreamResultBase<TConfig> {
211
+ type: 'ping';
212
+ }
202
213
 
203
214
  type ConsumerStreamResult<TConfig extends ConsumerConfig> =
204
215
  | ConsumerStreamResultData<TConfig>
205
216
  | ConsumerStreamResultError<TConfig>
206
217
  | ConsumerStreamResultOpened<TConfig>
207
- | ConsumerStreamResultConnecting<TConfig>;
218
+ | ConsumerStreamResultConnecting<TConfig>
219
+ | ConsumerStreamResultTimeout<TConfig>
220
+ | ConsumerStreamResultPing<TConfig>;
208
221
 
209
222
  export interface SSEStreamConsumerOptions<TConfig extends ConsumerConfig> {
210
223
  url: () => MaybePromise<string>;
@@ -214,6 +227,10 @@ export interface SSEStreamConsumerOptions<TConfig extends ConsumerConfig> {
214
227
  signal: AbortSignal;
215
228
  deserialize?: Deserialize;
216
229
  EventSource: TConfig['EventSource'];
230
+ /**
231
+ * Reconnect after inactivity in milliseconds
232
+ */
233
+ reconnectAfterInactivityMs?: number;
217
234
  }
218
235
 
219
236
  interface ConsumerConfig {
@@ -222,6 +239,31 @@ interface ConsumerConfig {
222
239
  EventSource: EventSourceLike.AnyConstructor;
223
240
  }
224
241
 
242
+ async function withTimeout<T>(opts: {
243
+ promise: Promise<T>;
244
+ timeoutMs: number;
245
+ onTimeout: () => Promise<NoInfer<T>>;
246
+ }): Promise<T> {
247
+ let timeoutId: ReturnType<typeof setTimeout>;
248
+
249
+ const timeoutPromise = new Promise<null>((resolve) => {
250
+ timeoutId = setTimeout(() => {
251
+ resolve(null);
252
+ }, opts.timeoutMs);
253
+ });
254
+ let res;
255
+ try {
256
+ res = await Unpromise.race([opts.promise, timeoutPromise]);
257
+ } finally {
258
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
259
+ clearTimeout(timeoutId!);
260
+ }
261
+ if (res === null) {
262
+ return await opts.onTimeout();
263
+ }
264
+ return res;
265
+ }
266
+
225
267
  /**
226
268
  * @see https://html.spec.whatwg.org/multipage/server-sent-events.html
227
269
  */
@@ -234,99 +276,145 @@ export function sseStreamConsumer<TConfig extends ConsumerConfig>(
234
276
 
235
277
  let _es: InstanceType<TConfig['EventSource']> | null = null;
236
278
 
237
- const stream = new ReadableStream<ConsumerStreamResult<TConfig>>({
238
- async start(controller) {
239
- const [url, init] = await Promise.all([opts.url(), opts.init()]);
240
- const eventSource = (_es = new opts.EventSource(
241
- url,
242
- init,
243
- ) as InstanceType<TConfig['EventSource']>);
244
-
245
- controller.enqueue({
246
- type: 'connecting',
247
- eventSource: _es,
248
- event: null,
249
- });
250
- eventSource.addEventListener('open', () => {
279
+ const createStream = () =>
280
+ new ReadableStream<ConsumerStreamResult<TConfig>>({
281
+ async start(controller) {
282
+ const [url, init] = await Promise.all([opts.url(), opts.init()]);
283
+ const eventSource = (_es = new opts.EventSource(
284
+ url,
285
+ init,
286
+ ) as InstanceType<TConfig['EventSource']>);
287
+
251
288
  controller.enqueue({
252
- type: 'opened',
253
- eventSource,
289
+ type: 'connecting',
290
+ eventSource: _es,
291
+ event: null,
292
+ });
293
+ eventSource.addEventListener('open', () => {
294
+ controller.enqueue({
295
+ type: 'opened',
296
+ eventSource,
297
+ });
254
298
  });
255
- });
256
299
 
257
- eventSource.addEventListener(SERIALIZED_ERROR_EVENT, (_msg) => {
258
- const msg = _msg as EventSourceLike.MessageEvent;
300
+ eventSource.addEventListener(SERIALIZED_ERROR_EVENT, (_msg) => {
301
+ const msg = _msg as EventSourceLike.MessageEvent;
259
302
 
260
- controller.enqueue({
261
- type: 'serialized-error',
262
- error: deserialize(JSON.parse(msg.data)),
263
- eventSource,
303
+ controller.enqueue({
304
+ type: 'serialized-error',
305
+ error: deserialize(JSON.parse(msg.data)),
306
+ eventSource,
307
+ });
264
308
  });
265
- });
266
- eventSource.addEventListener('error', (event) => {
267
- if (eventSource.readyState === EventSource.CLOSED) {
268
- controller.error(event);
269
- } else {
309
+ eventSource.addEventListener(PING_EVENT, () => {
270
310
  controller.enqueue({
271
- type: 'connecting',
311
+ type: 'ping',
272
312
  eventSource,
273
- event,
274
313
  });
275
- }
276
- });
277
- eventSource.addEventListener('message', (_msg) => {
278
- const msg = _msg as EventSourceLike.MessageEvent;
314
+ });
315
+ eventSource.addEventListener('error', (event) => {
316
+ if (eventSource.readyState === EventSource.CLOSED) {
317
+ controller.error(event);
318
+ } else {
319
+ controller.enqueue({
320
+ type: 'connecting',
321
+ eventSource,
322
+ event,
323
+ });
324
+ }
325
+ });
326
+ eventSource.addEventListener('message', (_msg) => {
327
+ const msg = _msg as EventSourceLike.MessageEvent;
279
328
 
280
- const chunk = deserialize(JSON.parse(msg.data));
329
+ const chunk = deserialize(JSON.parse(msg.data));
281
330
 
282
- const def: SSEvent = {
283
- data: chunk,
331
+ const def: SSEvent = {
332
+ data: chunk,
333
+ };
334
+ if (msg.lastEventId) {
335
+ def.id = msg.lastEventId;
336
+ }
337
+ controller.enqueue({
338
+ type: 'data',
339
+ data: def as inferTrackedOutput<TConfig['data']>,
340
+ eventSource,
341
+ });
342
+ });
343
+
344
+ const onAbort = () => {
345
+ controller.close();
346
+ eventSource.close();
284
347
  };
285
- if (msg.lastEventId) {
286
- def.id = msg.lastEventId;
348
+ if (signal.aborted) {
349
+ onAbort();
350
+ } else {
351
+ signal.addEventListener('abort', onAbort);
287
352
  }
288
- controller.enqueue({
289
- type: 'data',
290
- data: def as inferTrackedOutput<TConfig['data']>,
291
- eventSource,
292
- });
293
- });
353
+ },
354
+ cancel() {
355
+ _es?.close();
356
+ },
357
+ });
294
358
 
295
- const onAbort = () => {
296
- controller.close();
297
- eventSource.close();
298
- };
299
- if (signal.aborted) {
300
- onAbort();
301
- } else {
302
- signal.addEventListener('abort', onAbort);
303
- }
304
- },
305
- cancel() {
306
- _es?.close();
307
- },
308
- });
359
+ const getNewStreamAndReader = () => {
360
+ const stream = createStream();
361
+ const reader = stream.getReader();
362
+
363
+ return {
364
+ reader,
365
+ cancel: () => {
366
+ reader.releaseLock();
367
+ return stream.cancel();
368
+ },
369
+ };
370
+ };
309
371
  return {
310
372
  [Symbol.asyncIterator]() {
311
- const reader = stream.getReader();
373
+ let stream = getNewStreamAndReader();
312
374
 
313
375
  const iterator: AsyncIterator<ConsumerStreamResult<TConfig>> = {
314
376
  async next() {
315
- const value = await reader.read();
377
+ let promise = stream.reader.read();
378
+
379
+ if (opts.reconnectAfterInactivityMs) {
380
+ promise = withTimeout({
381
+ promise,
382
+ timeoutMs: opts.reconnectAfterInactivityMs,
383
+ onTimeout: async () => {
384
+ // Close and release old reader
385
+ await stream.cancel();
386
+
387
+ // Create new reader
388
+ stream = getNewStreamAndReader();
389
+
390
+ return {
391
+ value: {
392
+ type: 'timeout',
393
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
394
+ eventSource: _es!,
395
+ },
396
+ done: false,
397
+ };
398
+ },
399
+ });
400
+ }
401
+
402
+ const result = await promise;
316
403
 
317
- if (value.done) {
404
+ // console.debug('result', result, 'done', result.done);
405
+ if (result.done) {
318
406
  return {
319
- value: undefined,
407
+ value: result.value,
320
408
  done: true,
321
409
  };
322
410
  }
323
411
  return {
324
- value: value.value,
412
+ value: result.value,
325
413
  done: false,
326
414
  };
327
415
  },
328
416
  async return() {
329
- reader.releaseLock();
417
+ await stream.cancel();
330
418
  return {
331
419
  value: undefined,
332
420
  done: true,