@trpc/server 11.0.0-rc.630 → 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 (117) 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 +80 -0
  29. package/dist/adapters/node-http/writeResponse.mjs +77 -0
  30. package/dist/adapters/standalone.js +1 -0
  31. package/dist/adapters/standalone.mjs +1 -0
  32. package/dist/adapters/ws.js +1 -0
  33. package/dist/adapters/ws.mjs +1 -0
  34. package/dist/bundle-analysis.json +195 -168
  35. package/dist/http.js +1 -2
  36. package/dist/http.mjs +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.mjs +1 -0
  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/http/resolveResponse.d.ts.map +1 -1
  48. package/dist/unstable-core-do-not-import/http/resolveResponse.js +3 -3
  49. package/dist/unstable-core-do-not-import/http/resolveResponse.mjs +3 -3
  50. package/dist/unstable-core-do-not-import/initTRPC.js +2 -2
  51. package/dist/unstable-core-do-not-import/initTRPC.mjs +2 -2
  52. package/dist/unstable-core-do-not-import/rootConfig.d.ts +14 -14
  53. package/dist/unstable-core-do-not-import/rootConfig.d.ts.map +1 -1
  54. package/dist/unstable-core-do-not-import/rpc/envelopes.d.ts +7 -10
  55. package/dist/unstable-core-do-not-import/rpc/envelopes.d.ts.map +1 -1
  56. package/dist/unstable-core-do-not-import/stream/jsonl.d.ts +6 -9
  57. package/dist/unstable-core-do-not-import/stream/jsonl.d.ts.map +1 -1
  58. package/dist/unstable-core-do-not-import/stream/jsonl.js +75 -124
  59. package/dist/unstable-core-do-not-import/stream/jsonl.mjs +76 -125
  60. package/dist/unstable-core-do-not-import/stream/sse.d.ts +11 -1
  61. package/dist/unstable-core-do-not-import/stream/sse.d.ts.map +1 -1
  62. package/dist/unstable-core-do-not-import/stream/sse.js +154 -86
  63. package/dist/unstable-core-do-not-import/stream/sse.mjs +155 -87
  64. package/dist/unstable-core-do-not-import/stream/utils/asyncIterable.d.ts +10 -10
  65. package/dist/unstable-core-do-not-import/stream/utils/asyncIterable.d.ts.map +1 -1
  66. package/dist/unstable-core-do-not-import/stream/utils/asyncIterable.js +47 -34
  67. package/dist/unstable-core-do-not-import/stream/utils/asyncIterable.mjs +47 -34
  68. package/dist/unstable-core-do-not-import/stream/utils/createReadableStream.d.ts +0 -4
  69. package/dist/unstable-core-do-not-import/stream/utils/createReadableStream.d.ts.map +1 -1
  70. package/dist/unstable-core-do-not-import/stream/utils/createReadableStream.js +0 -11
  71. package/dist/unstable-core-do-not-import/stream/utils/createReadableStream.mjs +1 -11
  72. package/dist/unstable-core-do-not-import/stream/utils/disposablePromiseTimer.d.ts +6 -0
  73. package/dist/unstable-core-do-not-import/stream/utils/disposablePromiseTimer.d.ts.map +1 -0
  74. package/dist/unstable-core-do-not-import/stream/utils/disposablePromiseTimer.js +28 -0
  75. package/dist/unstable-core-do-not-import/stream/utils/disposablePromiseTimer.mjs +25 -0
  76. package/dist/unstable-core-do-not-import/stream/utils/withPing.d.ts.map +1 -1
  77. package/dist/unstable-core-do-not-import/stream/utils/withPing.js +17 -17
  78. package/dist/unstable-core-do-not-import/stream/utils/withPing.mjs +17 -17
  79. package/dist/unstable-core-do-not-import/stream/utils/withRefCount.d.ts +17 -0
  80. package/dist/unstable-core-do-not-import/stream/utils/withRefCount.d.ts.map +1 -0
  81. package/dist/unstable-core-do-not-import/stream/utils/withRefCount.js +59 -0
  82. package/dist/unstable-core-do-not-import/stream/utils/withRefCount.mjs +57 -0
  83. package/dist/unstable-core-do-not-import/transformer.d.ts +1 -4
  84. package/dist/unstable-core-do-not-import/transformer.d.ts.map +1 -1
  85. package/dist/unstable-core-do-not-import.d.ts +2 -2
  86. package/dist/unstable-core-do-not-import.d.ts.map +1 -1
  87. package/dist/unstable-core-do-not-import.js +2 -2
  88. package/dist/unstable-core-do-not-import.mjs +1 -1
  89. package/package.json +3 -3
  90. package/src/@trpc/server/http.ts +0 -1
  91. package/src/adapters/fastify/fastifyRequestHandler.ts +1 -1
  92. package/src/adapters/node-http/incomingMessageToRequest.ts +8 -4
  93. package/src/adapters/node-http/nodeHTTPRequestHandler.ts +8 -46
  94. package/src/adapters/node-http/writeResponse.ts +91 -0
  95. package/src/unstable-core-do-not-import/http/isAbortError.ts +7 -0
  96. package/src/unstable-core-do-not-import/http/resolveResponse.ts +3 -4
  97. package/src/unstable-core-do-not-import/initTRPC.ts +1 -1
  98. package/src/unstable-core-do-not-import/rootConfig.ts +17 -17
  99. package/src/unstable-core-do-not-import/rpc/envelopes.ts +7 -12
  100. package/src/unstable-core-do-not-import/stream/jsonl.ts +85 -154
  101. package/src/unstable-core-do-not-import/stream/sse.ts +179 -92
  102. package/src/unstable-core-do-not-import/stream/utils/asyncIterable.ts +58 -37
  103. package/src/unstable-core-do-not-import/stream/utils/createReadableStream.ts +0 -13
  104. package/src/unstable-core-do-not-import/stream/utils/disposablePromiseTimer.ts +27 -0
  105. package/src/unstable-core-do-not-import/stream/utils/withPing.ts +31 -19
  106. package/src/unstable-core-do-not-import/stream/utils/withRefCount.ts +93 -0
  107. package/src/unstable-core-do-not-import.ts +2 -2
  108. package/dist/unstable-core-do-not-import/http/batchStreamFormatter.d.ts +0 -24
  109. package/dist/unstable-core-do-not-import/http/batchStreamFormatter.d.ts.map +0 -1
  110. package/dist/unstable-core-do-not-import/http/batchStreamFormatter.js +0 -32
  111. package/dist/unstable-core-do-not-import/http/batchStreamFormatter.mjs +0 -30
  112. package/dist/unstable-core-do-not-import/stream/utils/promiseTimer.d.ts +0 -8
  113. package/dist/unstable-core-do-not-import/stream/utils/promiseTimer.d.ts.map +0 -1
  114. package/dist/unstable-core-do-not-import/stream/utils/promiseTimer.js +0 -38
  115. package/dist/unstable-core-do-not-import/stream/utils/promiseTimer.mjs +0 -36
  116. package/src/unstable-core-do-not-import/http/batchStreamFormatter.ts +0 -29
  117. package/src/unstable-core-do-not-import/stream/utils/promiseTimer.ts +0 -40
@@ -1,13 +1,13 @@
1
+ import { Unpromise } from '../../vendor/unpromise';
1
2
  import { getTRPCErrorFromUnknown } from '../error/TRPCError';
3
+ import { isAbortError } from '../http/isAbortError';
2
4
  import type { MaybePromise } from '../types';
3
5
  import { identity, run } from '../utils';
4
6
  import type { EventSourceLike } from './sse.types';
5
7
  import type { inferTrackedOutput } from './tracked';
6
8
  import { isTrackedEnvelope } from './tracked';
7
- import { takeWithGrace, withCancel } from './utils/asyncIterable';
9
+ import { takeWithGrace, withMaxDuration } from './utils/asyncIterable';
8
10
  import { createReadableStream } from './utils/createReadableStream';
9
- import type { PromiseTimer } from './utils/promiseTimer';
10
- import { createPromiseTimer } from './utils/promiseTimer';
11
11
  import { PING_SYM, withPing } from './utils/withPing';
12
12
 
13
13
  type Serialize = (value: any) => any;
@@ -50,6 +50,7 @@ export interface SSEStreamProducerOptions<TValue = unknown> {
50
50
  formatError?: (opts: { error: unknown }) => unknown;
51
51
  }
52
52
 
53
+ const PING_EVENT = 'ping';
53
54
  const SERIALIZED_ERROR_EVENT = 'serialized-error';
54
55
 
55
56
  type SSEvent = Partial<{
@@ -80,27 +81,23 @@ export function sseStreamProducer<TValue = unknown>(
80
81
 
81
82
  let iterable: AsyncIterable<TValue | typeof PING_SYM> = opts.data;
82
83
 
83
- iterable = withCancel(iterable, stream.cancelledPromise);
84
-
85
84
  if (opts.emitAndEndImmediately) {
86
85
  iterable = takeWithGrace(iterable, {
87
86
  count: 1,
88
87
  gracePeriodMs: 1,
89
- onCancel: () => opts.abortCtrl.abort(),
88
+ abortCtrl: opts.abortCtrl,
90
89
  });
91
90
  }
92
91
 
93
- let maxDurationTimer: PromiseTimer | null = null;
94
92
  if (
95
- opts.maxDurationMs != null &&
93
+ opts.maxDurationMs &&
96
94
  opts.maxDurationMs > 0 &&
97
95
  opts.maxDurationMs !== Infinity
98
96
  ) {
99
- maxDurationTimer = createPromiseTimer(opts.maxDurationMs).start();
100
- iterable = withCancel(
101
- iterable,
102
- maxDurationTimer.promise.then(() => opts.abortCtrl.abort()),
103
- );
97
+ iterable = withMaxDuration(iterable, {
98
+ maxDurationMs: opts.maxDurationMs,
99
+ abortCtrl: opts.abortCtrl,
100
+ });
104
101
  }
105
102
 
106
103
  if (ping.enabled && ping.intervalMs !== Infinity && ping.intervalMs > 0) {
@@ -115,7 +112,7 @@ export function sseStreamProducer<TValue = unknown>(
115
112
 
116
113
  for await (value of iterable) {
117
114
  if (value === PING_SYM) {
118
- stream.controller.enqueue({ comment: 'ping' });
115
+ stream.controller.enqueue({ event: PING_EVENT, data: '' });
119
116
  continue;
120
117
  }
121
118
 
@@ -133,20 +130,24 @@ export function sseStreamProducer<TValue = unknown>(
133
130
  chunk = null;
134
131
  }
135
132
  } 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
- });
133
+ if (isAbortError(err)) {
134
+ // ignore abort errors, send any other errors
135
+ return;
146
136
  }
137
+ // `err` must be caused by `opts.data`, `JSON.stringify` or `serialize`.
138
+ // So, a user error in any case.
139
+ const error = getTRPCErrorFromUnknown(err);
140
+ const data = opts.formatError?.({ error }) ?? null;
141
+ stream.controller.enqueue({
142
+ event: SERIALIZED_ERROR_EVENT,
143
+ data: JSON.stringify(serialize(data)),
144
+ });
147
145
  } finally {
148
- maxDurationTimer?.clear();
149
- stream.controller.close();
146
+ try {
147
+ stream.controller.close();
148
+ } catch {
149
+ // ignore
150
+ }
150
151
  }
151
152
  }).catch((err) => {
152
153
  // should not be reached; just in case...
@@ -200,12 +201,23 @@ interface ConsumerStreamResultConnecting<TConfig extends ConsumerConfig>
200
201
  type: 'connecting';
201
202
  event: EventSourceLike.EventOf<TConfig['EventSource']> | null;
202
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
+ }
203
213
 
204
214
  type ConsumerStreamResult<TConfig extends ConsumerConfig> =
205
215
  | ConsumerStreamResultData<TConfig>
206
216
  | ConsumerStreamResultError<TConfig>
207
217
  | ConsumerStreamResultOpened<TConfig>
208
- | ConsumerStreamResultConnecting<TConfig>;
218
+ | ConsumerStreamResultConnecting<TConfig>
219
+ | ConsumerStreamResultTimeout<TConfig>
220
+ | ConsumerStreamResultPing<TConfig>;
209
221
 
210
222
  export interface SSEStreamConsumerOptions<TConfig extends ConsumerConfig> {
211
223
  url: () => MaybePromise<string>;
@@ -215,6 +227,10 @@ export interface SSEStreamConsumerOptions<TConfig extends ConsumerConfig> {
215
227
  signal: AbortSignal;
216
228
  deserialize?: Deserialize;
217
229
  EventSource: TConfig['EventSource'];
230
+ /**
231
+ * Reconnect after inactivity in milliseconds
232
+ */
233
+ reconnectAfterInactivityMs?: number;
218
234
  }
219
235
 
220
236
  interface ConsumerConfig {
@@ -223,6 +239,31 @@ interface ConsumerConfig {
223
239
  EventSource: EventSourceLike.AnyConstructor;
224
240
  }
225
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
+
226
267
  /**
227
268
  * @see https://html.spec.whatwg.org/multipage/server-sent-events.html
228
269
  */
@@ -235,99 +276,145 @@ export function sseStreamConsumer<TConfig extends ConsumerConfig>(
235
276
 
236
277
  let _es: InstanceType<TConfig['EventSource']> | null = null;
237
278
 
238
- const stream = new ReadableStream<ConsumerStreamResult<TConfig>>({
239
- async start(controller) {
240
- const [url, init] = await Promise.all([opts.url(), opts.init()]);
241
- const eventSource = (_es = new opts.EventSource(
242
- url,
243
- init,
244
- ) as InstanceType<TConfig['EventSource']>);
245
-
246
- controller.enqueue({
247
- type: 'connecting',
248
- eventSource: _es,
249
- event: null,
250
- });
251
- 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
+
252
288
  controller.enqueue({
253
- type: 'opened',
254
- eventSource,
289
+ type: 'connecting',
290
+ eventSource: _es,
291
+ event: null,
292
+ });
293
+ eventSource.addEventListener('open', () => {
294
+ controller.enqueue({
295
+ type: 'opened',
296
+ eventSource,
297
+ });
255
298
  });
256
- });
257
299
 
258
- eventSource.addEventListener(SERIALIZED_ERROR_EVENT, (_msg) => {
259
- const msg = _msg as EventSourceLike.MessageEvent;
300
+ eventSource.addEventListener(SERIALIZED_ERROR_EVENT, (_msg) => {
301
+ const msg = _msg as EventSourceLike.MessageEvent;
260
302
 
261
- controller.enqueue({
262
- type: 'serialized-error',
263
- error: deserialize(JSON.parse(msg.data)),
264
- eventSource,
303
+ controller.enqueue({
304
+ type: 'serialized-error',
305
+ error: deserialize(JSON.parse(msg.data)),
306
+ eventSource,
307
+ });
265
308
  });
266
- });
267
- eventSource.addEventListener('error', (event) => {
268
- if (eventSource.readyState === EventSource.CLOSED) {
269
- controller.error(event);
270
- } else {
309
+ eventSource.addEventListener(PING_EVENT, () => {
271
310
  controller.enqueue({
272
- type: 'connecting',
311
+ type: 'ping',
273
312
  eventSource,
274
- event,
275
313
  });
276
- }
277
- });
278
- eventSource.addEventListener('message', (_msg) => {
279
- 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;
328
+
329
+ const chunk = deserialize(JSON.parse(msg.data));
280
330
 
281
- const chunk = deserialize(JSON.parse(msg.data));
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
+ });
282
343
 
283
- const def: SSEvent = {
284
- data: chunk,
344
+ const onAbort = () => {
345
+ controller.close();
346
+ eventSource.close();
285
347
  };
286
- if (msg.lastEventId) {
287
- def.id = msg.lastEventId;
348
+ if (signal.aborted) {
349
+ onAbort();
350
+ } else {
351
+ signal.addEventListener('abort', onAbort);
288
352
  }
289
- controller.enqueue({
290
- type: 'data',
291
- data: def as inferTrackedOutput<TConfig['data']>,
292
- eventSource,
293
- });
294
- });
353
+ },
354
+ cancel() {
355
+ _es?.close();
356
+ },
357
+ });
295
358
 
296
- const onAbort = () => {
297
- controller.close();
298
- eventSource.close();
299
- };
300
- if (signal.aborted) {
301
- onAbort();
302
- } else {
303
- signal.addEventListener('abort', onAbort);
304
- }
305
- },
306
- cancel() {
307
- _es?.close();
308
- },
309
- });
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
+ };
310
371
  return {
311
372
  [Symbol.asyncIterator]() {
312
- const reader = stream.getReader();
373
+ let stream = getNewStreamAndReader();
313
374
 
314
375
  const iterator: AsyncIterator<ConsumerStreamResult<TConfig>> = {
315
376
  async next() {
316
- 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;
317
403
 
318
- if (value.done) {
404
+ // console.debug('result', result, 'done', result.done);
405
+ if (result.done) {
319
406
  return {
320
- value: undefined,
407
+ value: result.value,
321
408
  done: true,
322
409
  };
323
410
  }
324
411
  return {
325
- value: value.value,
412
+ value: result.value,
326
413
  done: false,
327
414
  };
328
415
  },
329
416
  async return() {
330
- reader.releaseLock();
417
+ await stream.cancel();
331
418
  return {
332
419
  value: undefined,
333
420
  done: true,
@@ -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
  }
@@ -1,8 +1,3 @@
1
- import { createDeferred } from './createDeferred';
2
-
3
- // ---------- utils
4
-
5
- const cancelledStreamSymbol = Symbol();
6
1
  /**
7
2
  * One-off readable stream
8
3
  */
@@ -10,14 +5,12 @@ export function createReadableStream<TValue = unknown>() {
10
5
  let controller: ReadableStreamDefaultController<TValue> =
11
6
  null as unknown as ReadableStreamDefaultController<TValue>;
12
7
 
13
- const deferred = createDeferred<typeof cancelledStreamSymbol>();
14
8
  let cancelled = false;
15
9
  const readable = new ReadableStream<TValue>({
16
10
  start(c) {
17
11
  controller = c;
18
12
  },
19
13
  cancel() {
20
- deferred.resolve(cancelledStreamSymbol);
21
14
  cancelled = true;
22
15
  },
23
16
  });
@@ -25,14 +18,8 @@ export function createReadableStream<TValue = unknown>() {
25
18
  return {
26
19
  readable,
27
20
  controller,
28
- cancelledPromise: deferred.promise,
29
21
  cancelled() {
30
22
  return cancelled;
31
23
  },
32
24
  } as const;
33
25
  }
34
- export function isCancelledStreamResult(
35
- v: unknown,
36
- ): v is typeof cancelledStreamSymbol {
37
- return v === cancelledStreamSymbol;
38
- }
@@ -0,0 +1,27 @@
1
+ // @ts-expect-error polyfill
2
+ Symbol.dispose ??= Symbol();
3
+
4
+ export const disposablePromiseTimerResult = Symbol();
5
+ export function disposablePromiseTimer(ms: number) {
6
+ let timer: ReturnType<typeof setTimeout> | null = null;
7
+
8
+ return {
9
+ start() {
10
+ if (timer) {
11
+ throw new Error('Timer already started');
12
+ }
13
+
14
+ const promise = new Promise<typeof disposablePromiseTimerResult>(
15
+ (resolve) => {
16
+ timer = setTimeout(() => resolve(disposablePromiseTimerResult), ms);
17
+ },
18
+ );
19
+ return promise;
20
+ },
21
+ [Symbol.dispose]: () => {
22
+ if (timer) {
23
+ clearTimeout(timer);
24
+ }
25
+ },
26
+ };
27
+ }
@@ -1,13 +1,11 @@
1
1
  import { Unpromise } from '../../../vendor/unpromise';
2
- import { createPromiseTimer } from './promiseTimer';
2
+ import {
3
+ disposablePromiseTimer,
4
+ disposablePromiseTimerResult,
5
+ } from './disposablePromiseTimer';
3
6
 
4
7
  export const PING_SYM = Symbol('ping');
5
8
 
6
- const PING_RESULT: IteratorResult<typeof PING_SYM> = {
7
- value: PING_SYM,
8
- done: false,
9
- };
10
-
11
9
  /**
12
10
  * Derives a new {@link AsyncGenerator} based of {@link iterable}, that yields {@link PING_SYM}
13
11
  * whenever no value has been yielded for {@link pingIntervalMs}.
@@ -16,24 +14,38 @@ export async function* withPing<TValue>(
16
14
  iterable: AsyncIterable<TValue>,
17
15
  pingIntervalMs: number,
18
16
  ): AsyncGenerator<TValue | typeof PING_SYM> {
19
- const timer = createPromiseTimer(pingIntervalMs);
20
17
  const iterator = iterable[Symbol.asyncIterator]();
21
18
  // declaration outside the loop for garbage collection reasons
22
- let result: null | IteratorResult<TValue | typeof PING_SYM>;
19
+ let result:
20
+ | null
21
+ | IteratorResult<TValue>
22
+ | typeof disposablePromiseTimerResult;
23
+
24
+ let nextPromise = iterator.next();
23
25
  while (true) {
24
- const nextPromise = iterator.next();
25
- const pingPromise = timer.start().promise.then(() => PING_RESULT);
26
+ const pingPromise = disposablePromiseTimer(pingIntervalMs);
27
+
26
28
  try {
27
- result = await Unpromise.race([nextPromise, pingPromise]);
29
+ result = await Unpromise.race([nextPromise, pingPromise.start()]);
30
+
31
+ if (result === disposablePromiseTimerResult) {
32
+ // cancelled
33
+
34
+ yield PING_SYM;
35
+ continue;
36
+ }
37
+
38
+ if (result.done) {
39
+ return result.value;
40
+ }
41
+
42
+ nextPromise = iterator.next();
43
+ yield result.value;
44
+
45
+ // free up reference for garbage collection
46
+ result = null;
28
47
  } finally {
29
- timer.clear();
30
- }
31
- if (result.done) {
32
- return result.value;
48
+ pingPromise[Symbol.dispose]();
33
49
  }
34
- yield result.value;
35
- timer.reset();
36
- // free up reference for garbage collection
37
- result = null;
38
50
  }
39
51
  }