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

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 (101) hide show
  1. package/dist/@trpc/server/http.d.ts +1 -1
  2. package/dist/@trpc/server/http.d.ts.map +1 -1
  3. package/dist/adapters/aws-lambda/index.js +1 -0
  4. package/dist/adapters/aws-lambda/index.mjs +1 -0
  5. package/dist/adapters/express.js +1 -0
  6. package/dist/adapters/express.mjs +1 -0
  7. package/dist/adapters/fastify/fastifyRequestHandler.js +2 -1
  8. package/dist/adapters/fastify/fastifyRequestHandler.mjs +2 -1
  9. package/dist/adapters/fetch/fetchRequestHandler.js +1 -0
  10. package/dist/adapters/fetch/fetchRequestHandler.mjs +1 -0
  11. package/dist/adapters/next-app-dir/nextAppDirCaller.js +1 -0
  12. package/dist/adapters/next-app-dir/nextAppDirCaller.mjs +1 -0
  13. package/dist/adapters/next-app-dir/notFound.js +1 -0
  14. package/dist/adapters/next-app-dir/notFound.mjs +1 -0
  15. package/dist/adapters/next-app-dir/redirect.js +1 -0
  16. package/dist/adapters/next-app-dir/redirect.mjs +1 -0
  17. package/dist/adapters/next.js +1 -0
  18. package/dist/adapters/next.mjs +1 -0
  19. package/dist/adapters/node-http/incomingMessageToRequest.d.ts +1 -1
  20. package/dist/adapters/node-http/incomingMessageToRequest.d.ts.map +1 -1
  21. package/dist/adapters/node-http/incomingMessageToRequest.js +7 -5
  22. package/dist/adapters/node-http/incomingMessageToRequest.mjs +7 -5
  23. package/dist/adapters/node-http/nodeHTTPRequestHandler.d.ts.map +1 -1
  24. package/dist/adapters/node-http/nodeHTTPRequestHandler.js +9 -42
  25. package/dist/adapters/node-http/nodeHTTPRequestHandler.mjs +9 -42
  26. package/dist/adapters/node-http/writeResponse.d.ts +18 -0
  27. package/dist/adapters/node-http/writeResponse.d.ts.map +1 -0
  28. package/dist/adapters/node-http/writeResponse.js +85 -0
  29. package/dist/adapters/node-http/writeResponse.mjs +82 -0
  30. package/dist/adapters/standalone.js +1 -0
  31. package/dist/adapters/standalone.mjs +1 -0
  32. package/dist/adapters/ws.js +2 -1
  33. package/dist/adapters/ws.mjs +2 -1
  34. package/dist/bundle-analysis.json +146 -119
  35. package/dist/http.js +1 -2
  36. package/dist/http.mjs +1 -1
  37. package/dist/index.js +2 -1
  38. package/dist/index.mjs +2 -1
  39. package/dist/rpc.js +1 -0
  40. package/dist/rpc.mjs +1 -0
  41. package/dist/shared.js +1 -0
  42. package/dist/shared.mjs +1 -0
  43. package/dist/unstable-core-do-not-import/http/isAbortError.d.ts +4 -0
  44. package/dist/unstable-core-do-not-import/http/isAbortError.d.ts.map +1 -0
  45. package/dist/unstable-core-do-not-import/http/isAbortError.js +9 -0
  46. package/dist/unstable-core-do-not-import/http/isAbortError.mjs +7 -0
  47. package/dist/unstable-core-do-not-import/stream/jsonl.d.ts +6 -9
  48. package/dist/unstable-core-do-not-import/stream/jsonl.d.ts.map +1 -1
  49. package/dist/unstable-core-do-not-import/stream/jsonl.js +75 -124
  50. package/dist/unstable-core-do-not-import/stream/jsonl.mjs +76 -125
  51. package/dist/unstable-core-do-not-import/stream/sse.d.ts.map +1 -1
  52. package/dist/unstable-core-do-not-import/stream/sse.js +25 -21
  53. package/dist/unstable-core-do-not-import/stream/sse.mjs +26 -22
  54. package/dist/unstable-core-do-not-import/stream/utils/asyncIterable.d.ts +10 -10
  55. package/dist/unstable-core-do-not-import/stream/utils/asyncIterable.d.ts.map +1 -1
  56. package/dist/unstable-core-do-not-import/stream/utils/asyncIterable.js +47 -34
  57. package/dist/unstable-core-do-not-import/stream/utils/asyncIterable.mjs +47 -34
  58. package/dist/unstable-core-do-not-import/stream/utils/createReadableStream.d.ts +0 -4
  59. package/dist/unstable-core-do-not-import/stream/utils/createReadableStream.d.ts.map +1 -1
  60. package/dist/unstable-core-do-not-import/stream/utils/createReadableStream.js +0 -11
  61. package/dist/unstable-core-do-not-import/stream/utils/createReadableStream.mjs +1 -11
  62. package/dist/unstable-core-do-not-import/stream/utils/disposablePromiseTimer.d.ts +6 -0
  63. package/dist/unstable-core-do-not-import/stream/utils/disposablePromiseTimer.d.ts.map +1 -0
  64. package/dist/unstable-core-do-not-import/stream/utils/disposablePromiseTimer.js +28 -0
  65. package/dist/unstable-core-do-not-import/stream/utils/disposablePromiseTimer.mjs +25 -0
  66. package/dist/unstable-core-do-not-import/stream/utils/withPing.d.ts.map +1 -1
  67. package/dist/unstable-core-do-not-import/stream/utils/withPing.js +17 -17
  68. package/dist/unstable-core-do-not-import/stream/utils/withPing.mjs +17 -17
  69. package/dist/unstable-core-do-not-import/stream/utils/withRefCount.d.ts +17 -0
  70. package/dist/unstable-core-do-not-import/stream/utils/withRefCount.d.ts.map +1 -0
  71. package/dist/unstable-core-do-not-import/stream/utils/withRefCount.js +59 -0
  72. package/dist/unstable-core-do-not-import/stream/utils/withRefCount.mjs +57 -0
  73. package/dist/unstable-core-do-not-import.d.ts +2 -2
  74. package/dist/unstable-core-do-not-import.d.ts.map +1 -1
  75. package/dist/unstable-core-do-not-import.js +2 -2
  76. package/dist/unstable-core-do-not-import.mjs +1 -1
  77. package/package.json +3 -3
  78. package/src/@trpc/server/http.ts +0 -1
  79. package/src/adapters/fastify/fastifyRequestHandler.ts +1 -1
  80. package/src/adapters/node-http/incomingMessageToRequest.ts +8 -4
  81. package/src/adapters/node-http/nodeHTTPRequestHandler.ts +8 -46
  82. package/src/adapters/node-http/writeResponse.ts +96 -0
  83. package/src/unstable-core-do-not-import/http/isAbortError.ts +7 -0
  84. package/src/unstable-core-do-not-import/stream/jsonl.ts +85 -154
  85. package/src/unstable-core-do-not-import/stream/sse.ts +24 -25
  86. package/src/unstable-core-do-not-import/stream/utils/asyncIterable.ts +58 -37
  87. package/src/unstable-core-do-not-import/stream/utils/createReadableStream.ts +0 -13
  88. package/src/unstable-core-do-not-import/stream/utils/disposablePromiseTimer.ts +27 -0
  89. package/src/unstable-core-do-not-import/stream/utils/withPing.ts +31 -19
  90. package/src/unstable-core-do-not-import/stream/utils/withRefCount.ts +93 -0
  91. package/src/unstable-core-do-not-import.ts +2 -2
  92. package/dist/unstable-core-do-not-import/http/batchStreamFormatter.d.ts +0 -24
  93. package/dist/unstable-core-do-not-import/http/batchStreamFormatter.d.ts.map +0 -1
  94. package/dist/unstable-core-do-not-import/http/batchStreamFormatter.js +0 -32
  95. package/dist/unstable-core-do-not-import/http/batchStreamFormatter.mjs +0 -30
  96. package/dist/unstable-core-do-not-import/stream/utils/promiseTimer.d.ts +0 -8
  97. package/dist/unstable-core-do-not-import/stream/utils/promiseTimer.d.ts.map +0 -1
  98. package/dist/unstable-core-do-not-import/stream/utils/promiseTimer.js +0 -38
  99. package/dist/unstable-core-do-not-import/stream/utils/promiseTimer.mjs +0 -36
  100. package/src/unstable-core-do-not-import/http/batchStreamFormatter.ts +0 -29
  101. package/src/unstable-core-do-not-import/stream/utils/promiseTimer.ts +0 -40
@@ -1,12 +1,8 @@
1
- import { Unpromise } from '../../vendor/unpromise';
2
- import { getTRPCErrorFromUnknown } from '../error/TRPCError';
3
1
  import { isAsyncIterable, isFunction, isObject, run } from '../utils';
4
2
  import type { Deferred } from './utils/createDeferred';
5
3
  import { createDeferred } from './utils/createDeferred';
6
- import {
7
- createReadableStream,
8
- isCancelledStreamResult,
9
- } from './utils/createReadableStream';
4
+ import { createReadableStream } from './utils/createReadableStream';
5
+ import { withRefCount } from './utils/withRefCount';
10
6
 
11
7
  /**
12
8
  * A subset of the standard ReadableStream properties needed by tRPC internally.
@@ -58,31 +54,31 @@ type ChunkDefinition = [
58
54
  type: ChunkValueType,
59
55
  chunkId: ChunkIndex,
60
56
  ];
61
- type DehydratedValue = [
57
+ type EncodedValue = [
62
58
  // data
63
59
  [unknown] | [],
64
60
  // chunk descriptions
65
61
  ...ChunkDefinition[],
66
62
  ];
67
63
 
68
- type Head = Record<string, DehydratedValue>;
64
+ type Head = Record<string, EncodedValue>;
69
65
  type PromiseChunk =
70
66
  | [
71
67
  chunkIndex: ChunkIndex,
72
68
  status: PROMISE_STATUS_FULFILLED,
73
- value: DehydratedValue,
69
+ value: EncodedValue,
74
70
  ]
75
71
  | [chunkIndex: ChunkIndex, status: PROMISE_STATUS_REJECTED, error: unknown];
76
72
  type IterableChunk =
77
73
  | [
78
74
  chunkIndex: ChunkIndex,
79
75
  status: ASYNC_ITERABLE_STATUS_RETURN,
80
- value: DehydratedValue,
76
+ value: EncodedValue,
81
77
  ]
82
78
  | [
83
79
  chunkIndex: ChunkIndex,
84
80
  status: ASYNC_ITERABLE_STATUS_VALUE,
85
- value: DehydratedValue,
81
+ value: EncodedValue,
86
82
  ]
87
83
  | [
88
84
  chunkIndex: ChunkIndex,
@@ -127,42 +123,38 @@ function createBatchStreamProducer(opts: ProducerOptions) {
127
123
  const placeholder = 0 as PlaceholderValue;
128
124
 
129
125
  const stream = createReadableStream<ChunkData>();
130
- const pending = new Set<ChunkIndex>();
131
-
132
- function maybeClose() {
133
- if (pending.size === 0 && !stream.cancelled()) {
126
+ const pending = withRefCount(new Set<ChunkIndex>(), () => {
127
+ if (!stream.cancelled()) {
134
128
  stream.controller.close();
135
129
  }
136
- }
137
- function dehydratePromise(
138
- promise: Promise<unknown>,
139
- path: (string | number)[],
140
- ) {
141
- //
130
+ });
131
+
132
+ const maybeEnqueue = (chunk: ChunkData) => {
133
+ if (!stream.cancelled()) {
134
+ stream.controller.enqueue(chunk);
135
+ }
136
+ };
137
+
138
+ function encodePromise(promise: Promise<unknown>, path: (string | number)[]) {
142
139
  const error = checkMaxDepth(path);
143
140
  if (error) {
144
- promise.catch(() => {
145
- // ignore
141
+ // Catch any errors from the original promise to ensure they're reported
142
+ promise.catch((cause) => {
143
+ opts.onError?.({ error: cause, path });
146
144
  });
145
+ // Replace the promise with a rejected one containing the max depth error
147
146
  promise = Promise.reject(error);
148
147
  }
149
148
  const idx = counter++ as ChunkIndex;
150
149
  pending.add(idx);
151
150
 
152
- Unpromise.race([promise, stream.cancelledPromise])
151
+ promise
153
152
  .then((it) => {
154
- if (isCancelledStreamResult(it)) {
155
- return;
156
- }
157
- stream.controller.enqueue([
158
- idx,
159
- PROMISE_STATUS_FULFILLED,
160
- dehydrate(it, path),
161
- ]);
153
+ maybeEnqueue([idx, PROMISE_STATUS_FULFILLED, encode(it, path)]);
162
154
  })
163
155
  .catch((cause) => {
164
156
  opts.onError?.({ error: cause, path });
165
- stream.controller.enqueue([
157
+ maybeEnqueue([
166
158
  idx,
167
159
  PROMISE_STATUS_REJECTED,
168
160
  opts.formatError?.({ error: cause, path }),
@@ -170,78 +162,55 @@ function createBatchStreamProducer(opts: ProducerOptions) {
170
162
  })
171
163
  .finally(() => {
172
164
  pending.delete(idx);
173
- maybeClose();
174
165
  });
175
166
  return idx;
176
167
  }
177
- function dehydrateAsyncIterable(
168
+ function encodeAsyncIterable(
178
169
  iterable: AsyncIterable<unknown>,
179
170
  path: (string | number)[],
180
171
  ) {
181
- const error = checkMaxDepth(path);
182
- if (error) {
183
- iterable = {
184
- [Symbol.asyncIterator]() {
185
- throw error;
186
- },
187
- };
188
- }
189
172
  const idx = counter++ as ChunkIndex;
190
173
  pending.add(idx);
191
174
  run(async () => {
175
+ const error = checkMaxDepth(path);
176
+ if (error) {
177
+ throw error;
178
+ }
192
179
  const iterator = iterable[Symbol.asyncIterator]();
193
180
 
194
181
  while (true) {
195
- const next = await Unpromise.race([
196
- iterator.next().catch(getTRPCErrorFromUnknown),
197
- stream.cancelledPromise,
198
- ]);
199
-
200
- if (next instanceof Error) {
201
- opts.onError?.({ error: next, path });
202
-
203
- stream.controller.enqueue([
204
- idx,
205
- ASYNC_ITERABLE_STATUS_ERROR,
206
- opts.formatError?.({ error: next, path }),
207
- ]);
208
- return;
209
- }
210
- if (isCancelledStreamResult(next)) {
211
- await iterator.return?.();
212
- break;
182
+ if (stream.cancelled()) {
183
+ const res = await iterator.return?.();
184
+ return res?.value;
213
185
  }
186
+ const next = await iterator.next();
187
+
214
188
  if (next.done) {
215
- stream.controller.enqueue([
189
+ maybeEnqueue([
216
190
  idx,
217
191
  ASYNC_ITERABLE_STATUS_RETURN,
218
- dehydrate(next.value, path),
192
+ encode(next.value, path),
219
193
  ]);
220
194
  break;
221
195
  }
222
- stream.controller.enqueue([
196
+ maybeEnqueue([
223
197
  idx,
224
198
  ASYNC_ITERABLE_STATUS_VALUE,
225
- dehydrate(next.value, path),
199
+ encode(next.value, path),
226
200
  ]);
227
201
  }
228
-
229
- pending.delete(idx);
230
- maybeClose();
231
- }).catch((cause) => {
232
- // this shouldn't happen, but node crashes if we don't catch it
233
- opts.onError?.({
234
- error: new Error(
235
- 'You found a bug - please report it on https://github.com/trpc/trpc',
236
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
237
- // @ts-ignore https://github.com/tc39/proposal-error-cause
238
- {
239
- cause,
240
- },
241
- ),
242
- path,
202
+ })
203
+ .catch((cause) => {
204
+ opts.onError?.({ error: cause, path });
205
+ maybeEnqueue([
206
+ idx,
207
+ ASYNC_ITERABLE_STATUS_ERROR,
208
+ opts.formatError?.({ error: cause, path }),
209
+ ]);
210
+ })
211
+ .finally(() => {
212
+ pending.delete(idx);
243
213
  });
244
- });
245
214
  return idx;
246
215
  }
247
216
  function checkMaxDepth(path: (string | number)[]) {
@@ -250,12 +219,12 @@ function createBatchStreamProducer(opts: ProducerOptions) {
250
219
  }
251
220
  return null;
252
221
  }
253
- function dehydrateAsync(
222
+ function encodeAsync(
254
223
  value: unknown,
255
224
  path: (string | number)[],
256
225
  ): null | [type: ChunkValueType, chunkId: ChunkIndex] {
257
226
  if (isPromise(value)) {
258
- return [CHUNK_VALUE_TYPE_PROMISE, dehydratePromise(value, path)];
227
+ return [CHUNK_VALUE_TYPE_PROMISE, encodePromise(value, path)];
259
228
  }
260
229
  if (isAsyncIterable(value)) {
261
230
  if (opts.maxDepth && path.length >= opts.maxDepth) {
@@ -263,29 +232,26 @@ function createBatchStreamProducer(opts: ProducerOptions) {
263
232
  }
264
233
  return [
265
234
  CHUNK_VALUE_TYPE_ASYNC_ITERABLE,
266
- dehydrateAsyncIterable(value, path),
235
+ encodeAsyncIterable(value, path),
267
236
  ];
268
237
  }
269
238
  return null;
270
239
  }
271
- function dehydrate(
272
- value: unknown,
273
- path: (string | number)[],
274
- ): DehydratedValue {
240
+ function encode(value: unknown, path: (string | number)[]): EncodedValue {
275
241
  if (value === undefined) {
276
242
  return [[]];
277
243
  }
278
244
  if (!isObject(value)) {
279
245
  return [[value]];
280
246
  }
281
- const reg = dehydrateAsync(value, path);
247
+ const reg = encodeAsync(value, path);
282
248
  if (reg) {
283
249
  return [[placeholder], [null, ...reg]];
284
250
  }
285
251
  const newObj = {} as Record<string, unknown>;
286
252
  const asyncValues: ChunkDefinition[] = [];
287
253
  for (const [key, item] of Object.entries(value)) {
288
- const transformed = dehydrateAsync(item, [...path, key]);
254
+ const transformed = encodeAsync(item, [...path, key]);
289
255
  if (!transformed) {
290
256
  newObj[key] = item;
291
257
  continue;
@@ -298,8 +264,9 @@ function createBatchStreamProducer(opts: ProducerOptions) {
298
264
 
299
265
  const newHead: Head = {};
300
266
  for (const [key, item] of Object.entries(data)) {
301
- newHead[key] = dehydrate(item, [key]);
267
+ newHead[key] = encode(item, [key]);
302
268
  }
269
+ pending.activate();
303
270
 
304
271
  return [newHead, stream.readable] as const;
305
272
  }
@@ -440,7 +407,7 @@ export async function jsonlStreamConsumer<THead>(opts: {
440
407
  /**
441
408
  * This `AbortController` will be triggered when there are no more listeners to the stream.
442
409
  */
443
- abortController: AbortController | null;
410
+ abortController: AbortController;
444
411
  }) {
445
412
  const { deserialize = (v) => v } = opts;
446
413
 
@@ -458,46 +425,26 @@ export async function jsonlStreamConsumer<THead>(opts: {
458
425
 
459
426
  type ControllerChunk = ChunkData | StreamInterruptedError;
460
427
  type ChunkController = ReadableStreamDefaultController<ControllerChunk>;
461
- type ControllerWrapper = {
462
- controller: ChunkController;
463
- returned: boolean;
464
- };
465
- const chunkDeferred = new Map<ChunkIndex, Deferred<ControllerWrapper>>();
466
- const controllers = new Map<ChunkIndex, ControllerWrapper>();
467
-
468
- const maybeAbort = () => {
469
- if (
470
- chunkDeferred.size === 0 &&
471
- Array.from(controllers.values()).every((it) => it.returned)
472
- ) {
473
- // nothing is listening to the stream anymore
428
+
429
+ const controllers = withRefCount(
430
+ new Map<ChunkIndex, ChunkController>(),
431
+ () => {
474
432
  opts.abortController?.abort();
475
- }
476
- };
433
+ },
434
+ );
477
435
 
478
- function hydrateChunkDefinition(value: ChunkDefinition) {
436
+ function decodeChunkDefinition(value: ChunkDefinition) {
479
437
  const [_path, type, chunkId] = value;
480
438
 
481
- const { readable, controller } = createReadableStream<ChunkData>();
482
-
483
- const wrapper: ControllerWrapper = {
484
- controller,
485
- returned: false,
486
- };
487
- controllers.set(chunkId, wrapper);
439
+ const stream = createReadableStream<ChunkData>();
488
440
 
489
- // resolve chunk deferred if it exists
490
- const deferred = chunkDeferred.get(chunkId);
491
- if (deferred) {
492
- deferred.resolve(wrapper);
493
- chunkDeferred.delete(chunkId);
494
- }
441
+ controllers.set(chunkId, stream.controller);
495
442
 
496
443
  switch (type) {
497
444
  case CHUNK_VALUE_TYPE_PROMISE: {
498
445
  return new Promise((resolve, reject) => {
499
446
  // listen for next value in the stream
500
- const reader = readable.getReader();
447
+ const reader = stream.readable.getReader();
501
448
  reader
502
449
  .read()
503
450
  .then((it) => {
@@ -513,7 +460,7 @@ export async function jsonlStreamConsumer<THead>(opts: {
513
460
  const [_chunkId, status, data] = value as PromiseChunk;
514
461
  switch (status) {
515
462
  case PROMISE_STATUS_FULFILLED:
516
- resolve(hydrate(data));
463
+ resolve(decode(data));
517
464
 
518
465
  break;
519
466
  case PROMISE_STATUS_REJECTED:
@@ -525,17 +472,14 @@ export async function jsonlStreamConsumer<THead>(opts: {
525
472
  })
526
473
  .catch(reject)
527
474
  .finally(() => {
528
- // reader.releaseLock();
529
475
  controllers.delete(chunkId);
530
-
531
- maybeAbort();
532
476
  });
533
477
  });
534
478
  }
535
479
  case CHUNK_VALUE_TYPE_ASYNC_ITERABLE: {
536
480
  return {
537
481
  [Symbol.asyncIterator]: () => {
538
- const reader = readable.getReader();
482
+ const reader = stream.readable.getReader();
539
483
  const iterator: AsyncIterator<unknown> = {
540
484
  next: async () => {
541
485
  const { done, value } = await reader.read();
@@ -544,7 +488,6 @@ export async function jsonlStreamConsumer<THead>(opts: {
544
488
  }
545
489
  if (done) {
546
490
  controllers.delete(chunkId);
547
- maybeAbort();
548
491
  return {
549
492
  done: true,
550
493
  value: undefined,
@@ -557,18 +500,16 @@ export async function jsonlStreamConsumer<THead>(opts: {
557
500
  case ASYNC_ITERABLE_STATUS_VALUE:
558
501
  return {
559
502
  done: false,
560
- value: hydrate(data),
503
+ value: decode(data),
561
504
  };
562
505
  case ASYNC_ITERABLE_STATUS_RETURN:
563
506
  controllers.delete(chunkId);
564
- maybeAbort();
565
507
  return {
566
508
  done: true,
567
- value: hydrate(data),
509
+ value: decode(data),
568
510
  };
569
511
  case ASYNC_ITERABLE_STATUS_ERROR:
570
512
  controllers.delete(chunkId);
571
- maybeAbort();
572
513
  throw (
573
514
  opts.formatError?.({ error: data }) ??
574
515
  new AsyncError(data)
@@ -576,8 +517,7 @@ export async function jsonlStreamConsumer<THead>(opts: {
576
517
  }
577
518
  },
578
519
  return: async () => {
579
- wrapper.returned = true;
580
- maybeAbort();
520
+ controllers.delete(chunkId);
581
521
  return {
582
522
  done: true,
583
523
  value: undefined,
@@ -591,18 +531,18 @@ export async function jsonlStreamConsumer<THead>(opts: {
591
531
  }
592
532
  }
593
533
 
594
- function hydrate(value: DehydratedValue): unknown {
534
+ function decode(value: EncodedValue): unknown {
595
535
  const [[data], ...asyncProps] = value;
596
536
 
597
537
  for (const value of asyncProps) {
598
538
  const [key] = value;
599
- const hydrated = hydrateChunkDefinition(value);
539
+ const decoded = decodeChunkDefinition(value);
600
540
 
601
541
  if (key === null) {
602
- return hydrated;
542
+ return decoded;
603
543
  }
604
544
 
605
- (data as any)[key] = hydrated;
545
+ (data as any)[key] = decoded;
606
546
  }
607
547
  return data;
608
548
  }
@@ -611,11 +551,7 @@ export async function jsonlStreamConsumer<THead>(opts: {
611
551
  const error = new StreamInterruptedError(reason);
612
552
 
613
553
  headDeferred?.reject(error);
614
- for (const deferred of chunkDeferred.values()) {
615
- deferred.reject(error);
616
- }
617
- chunkDeferred.clear();
618
- for (const { controller } of controllers.values()) {
554
+ for (const controller of controllers.values()) {
619
555
  controller.enqueue(error);
620
556
  controller.close();
621
557
  }
@@ -629,26 +565,21 @@ export async function jsonlStreamConsumer<THead>(opts: {
629
565
  const head = chunkOrHead as Record<number | string, unknown>;
630
566
 
631
567
  for (const [key, value] of Object.entries(chunkOrHead)) {
632
- const parsed = hydrate(value as any);
568
+ const parsed = decode(value as any);
633
569
  head[key] = parsed;
634
570
  }
635
571
  headDeferred.resolve(head as THead);
636
572
  headDeferred = null;
573
+
574
+ controllers.activate();
637
575
  return;
638
576
  }
639
577
  const chunk = chunkOrHead as ChunkData;
640
578
  const [idx] = chunk;
641
- let wrapper = controllers.get(idx);
642
- if (!wrapper) {
643
- let deferred = chunkDeferred.get(idx);
644
- if (!deferred) {
645
- deferred = createDeferred();
646
- chunkDeferred.set(idx, deferred);
647
- }
648
579
 
649
- wrapper = await deferred.promise;
650
- }
651
- wrapper.controller.enqueue(chunk);
580
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
581
+ const controller = controllers.get(idx)!;
582
+ controller.enqueue(chunk);
652
583
  },
653
584
  close: closeOrAbort,
654
585
  abort: closeOrAbort,
@@ -1,13 +1,12 @@
1
1
  import { getTRPCErrorFromUnknown } from '../error/TRPCError';
2
+ import { isAbortError } from '../http/isAbortError';
2
3
  import type { MaybePromise } from '../types';
3
4
  import { identity, run } from '../utils';
4
5
  import type { EventSourceLike } from './sse.types';
5
6
  import type { inferTrackedOutput } from './tracked';
6
7
  import { isTrackedEnvelope } from './tracked';
7
- import { takeWithGrace, withCancel } from './utils/asyncIterable';
8
+ import { takeWithGrace, withMaxDuration } from './utils/asyncIterable';
8
9
  import { createReadableStream } from './utils/createReadableStream';
9
- import type { PromiseTimer } from './utils/promiseTimer';
10
- import { createPromiseTimer } from './utils/promiseTimer';
11
10
  import { PING_SYM, withPing } from './utils/withPing';
12
11
 
13
12
  type Serialize = (value: any) => any;
@@ -80,27 +79,23 @@ export function sseStreamProducer<TValue = unknown>(
80
79
 
81
80
  let iterable: AsyncIterable<TValue | typeof PING_SYM> = opts.data;
82
81
 
83
- iterable = withCancel(iterable, stream.cancelledPromise);
84
-
85
82
  if (opts.emitAndEndImmediately) {
86
83
  iterable = takeWithGrace(iterable, {
87
84
  count: 1,
88
85
  gracePeriodMs: 1,
89
- onCancel: () => opts.abortCtrl.abort(),
86
+ abortCtrl: opts.abortCtrl,
90
87
  });
91
88
  }
92
89
 
93
- let maxDurationTimer: PromiseTimer | null = null;
94
90
  if (
95
- opts.maxDurationMs != null &&
91
+ opts.maxDurationMs &&
96
92
  opts.maxDurationMs > 0 &&
97
93
  opts.maxDurationMs !== Infinity
98
94
  ) {
99
- maxDurationTimer = createPromiseTimer(opts.maxDurationMs).start();
100
- iterable = withCancel(
101
- iterable,
102
- maxDurationTimer.promise.then(() => opts.abortCtrl.abort()),
103
- );
95
+ iterable = withMaxDuration(iterable, {
96
+ maxDurationMs: opts.maxDurationMs,
97
+ abortCtrl: opts.abortCtrl,
98
+ });
104
99
  }
105
100
 
106
101
  if (ping.enabled && ping.intervalMs !== Infinity && ping.intervalMs > 0) {
@@ -133,20 +128,24 @@ export function sseStreamProducer<TValue = unknown>(
133
128
  chunk = null;
134
129
  }
135
130
  } catch (err) {
136
- // ignore abort errors, send any other errors
137
- if (!(err instanceof Error) || err.name !== 'AbortError') {
138
- // `err` must be caused by `opts.data`, `JSON.stringify` or `serialize`.
139
- // So, a user error in any case.
140
- const error = getTRPCErrorFromUnknown(err);
141
- const data = opts.formatError?.({ error }) ?? null;
142
- stream.controller.enqueue({
143
- event: SERIALIZED_ERROR_EVENT,
144
- data: JSON.stringify(serialize(data)),
145
- });
131
+ if (isAbortError(err)) {
132
+ // ignore abort errors, send any other errors
133
+ return;
146
134
  }
135
+ // `err` must be caused by `opts.data`, `JSON.stringify` or `serialize`.
136
+ // So, a user error in any case.
137
+ const error = getTRPCErrorFromUnknown(err);
138
+ const data = opts.formatError?.({ error }) ?? null;
139
+ stream.controller.enqueue({
140
+ event: SERIALIZED_ERROR_EVENT,
141
+ data: JSON.stringify(serialize(data)),
142
+ });
147
143
  } finally {
148
- maxDurationTimer?.clear();
149
- stream.controller.close();
144
+ try {
145
+ stream.controller.close();
146
+ } catch {
147
+ // ignore
148
+ }
150
149
  }
151
150
  }).catch((err) => {
152
151
  // should not be reached; just in case...
@@ -1,40 +1,47 @@
1
1
  import { Unpromise } from '../../../vendor/unpromise';
2
- import { noop } from '../../utils';
3
- import { createPromiseTimer } from './promiseTimer';
2
+ import {
3
+ disposablePromiseTimer,
4
+ disposablePromiseTimerResult,
5
+ } from './disposablePromiseTimer';
4
6
 
5
7
  /**
6
- * Derives a new {@link AsyncGenerator} based of {@link iterable}, that automatically stops with the
7
- * passed {@link cancel} promise.
8
+ * Derives a new {@link AsyncGenerator} based on {@link iterable}, that automatically stops after the specified duration.
8
9
  */
9
- export async function* withCancel<T>(
10
+ export async function* withMaxDuration<T>(
10
11
  iterable: AsyncIterable<T>,
11
- cancel: Promise<unknown>,
12
+ opts: { maxDurationMs: number; abortCtrl: AbortController },
12
13
  ): AsyncGenerator<T> {
13
- const cancelPromise = cancel.then(noop);
14
14
  const iterator = iterable[Symbol.asyncIterator]();
15
- // declaration outside the loop for garbage collection reasons
16
- let result: null | IteratorResult<T> | void;
17
- while (true) {
18
- result = await Unpromise.race([iterator.next(), cancelPromise]);
19
- if (result == null) {
20
- await iterator.return?.();
21
- break;
22
- }
23
- if (result.done) {
24
- break;
15
+
16
+ const timer = disposablePromiseTimer(opts.maxDurationMs);
17
+ try {
18
+ const timerPromise = timer.start();
19
+
20
+ // declaration outside the loop for garbage collection reasons
21
+ let result: null | IteratorResult<T> | typeof disposablePromiseTimerResult;
22
+
23
+ while (true) {
24
+ result = await Unpromise.race([iterator.next(), timerPromise]);
25
+ if (result === disposablePromiseTimerResult) {
26
+ // cancelled due to timeout
27
+ opts.abortCtrl.abort();
28
+ const res = await iterator.return?.();
29
+ return res?.value;
30
+ }
31
+ if (result.done) {
32
+ return result;
33
+ }
34
+ yield result.value;
35
+ // free up reference for garbage collection
36
+ result = null;
25
37
  }
26
- yield result.value;
27
- // free up reference for garbage collection
28
- result = null;
38
+ } finally {
39
+ // dispose timer
40
+ // Shouldn't be needed, but build breaks with `using` keyword
41
+ timer[Symbol.dispose]();
29
42
  }
30
43
  }
31
44
 
32
- interface TakeWithGraceOptions {
33
- count: number;
34
- gracePeriodMs: number;
35
- onCancel: () => void;
36
- }
37
-
38
45
  /**
39
46
  * Derives a new {@link AsyncGenerator} based of {@link iterable}, that yields its first
40
47
  * {@link count} values. Then, a grace period of {@link gracePeriodMs} is started in which further
@@ -42,31 +49,45 @@ interface TakeWithGraceOptions {
42
49
  */
43
50
  export async function* takeWithGrace<T>(
44
51
  iterable: AsyncIterable<T>,
45
- { count, gracePeriodMs, onCancel }: TakeWithGraceOptions,
52
+ opts: {
53
+ count: number;
54
+ gracePeriodMs: number;
55
+ abortCtrl: AbortController;
56
+ },
46
57
  ): AsyncGenerator<T> {
47
58
  const iterator = iterable[Symbol.asyncIterator]();
48
- const timer = createPromiseTimer(gracePeriodMs);
59
+
60
+ // declaration outside the loop for garbage collection reasons
61
+ let result: null | IteratorResult<T> | typeof disposablePromiseTimerResult;
62
+
63
+ const timer = disposablePromiseTimer(opts.gracePeriodMs);
49
64
  try {
50
- // declaration outside the loop for garbage collection reasons
51
- let result: null | IteratorResult<T> | void;
65
+ let count = opts.count;
66
+
67
+ let timerPromise = new Promise<typeof disposablePromiseTimerResult>(() => {
68
+ // never resolves
69
+ });
70
+
52
71
  while (true) {
53
- result = await Unpromise.race([iterator.next(), timer.promise]);
54
- if (result == null) {
72
+ result = await Unpromise.race([iterator.next(), timerPromise]);
73
+ if (result === disposablePromiseTimerResult) {
55
74
  // cancelled
56
- await iterator.return?.();
57
- break;
75
+ const res = await iterator.return?.();
76
+ return res?.value;
58
77
  }
59
78
  if (result.done) {
60
- break;
79
+ return result.value;
61
80
  }
62
81
  yield result.value;
63
82
  if (--count === 0) {
64
- timer.start().promise.then(onCancel, noop);
83
+ timerPromise = timer.start();
84
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
85
+ timerPromise.then(() => opts.abortCtrl.abort());
65
86
  }
66
87
  // free up reference for garbage collection
67
88
  result = null;
68
89
  }
69
90
  } finally {
70
- timer.clear();
91
+ timer[Symbol.dispose]();
71
92
  }
72
93
  }