GeezerVison is showing github.com — stream: reject iter consumers on abort · nodejs/node@ab5ed72
File treeExpand file treeCollapse file tree Open diff view settings lib/internal/streams/iter
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: lib/internal/streams/iter/consumers.js +6-2Lines changed: 6 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -52,6 +52,7 @@ const { | 52 | 52 |
| 53 | 53 | const { | 54 | 54 | concatBytes, | | 55 | + yieldAbortable, | 55 | 56 | } = require('internal/streams/iter/utils'); | 56 | 57 |
| 57 | 58 | const { | @@ -125,7 +126,9 @@ async function collectAsync(source, signal, limit) { | 125 | 126 | signal?.throwIfAborted(); | 126 | 127 |
| 127 | 128 | // Normalize source via from() - accepts strings, ArrayBuffers, protocols, etc. | 128 | | - const normalized = from(source); | | 129 | + const abortableSource = signal && isAsyncIterable(source) ? | | 130 | + yieldAbortable(source, signal) : source; | | 131 | + const normalized = from(abortableSource); | 129 | 132 | const chunks = []; | 130 | 133 |
| 131 | 134 | // Fast path: no signal and no limit | @@ -140,8 +143,9 @@ async function collectAsync(source, signal, limit) { | 140 | 143 |
| 141 | 144 | // Slow path: with signal or limit checks | 142 | 145 | let totalBytes = 0; | | 146 | + const iterable = signal ? yieldAbortable(normalized, signal) : normalized; | 143 | 147 |
| 144 | | - for await (const batch of normalized) { | | 148 | + for await (const batch of iterable) { | 145 | 149 | signal?.throwIfAborted(); | 146 | 150 | for (let i = 0; i < batch.length; i++) { | 147 | 151 | const chunk = batch[i]; | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/streams/iter/pull.js +1-82Lines changed: 1 addition & 82 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -14,19 +14,12 @@ const { | 14 | 14 | ArrayPrototypeSlice, | 15 | 15 | PromisePrototypeThen, | 16 | 16 | PromiseResolve, | 17 | | - PromiseWithResolvers, | 18 | | - SafePromisePrototypeFinally, | 19 | | - SafePromiseRace, | 20 | 17 | SymbolAsyncIterator, | 21 | 18 | SymbolIterator, | 22 | 19 | TypedArrayPrototypeGetByteLength, | 23 | 20 | Uint8Array, | 24 | 21 | } = primordials; | 25 | 22 |
| 26 | | -const { | 27 | | - markPromiseAsHandled, | 28 | | -} = internalBinding('util'); | 29 | | - | 30 | 23 | const { | 31 | 24 | codes: { | 32 | 25 | ERR_INVALID_ARG_TYPE, | @@ -60,6 +53,7 @@ const { | 60 | 53 | parsePullArgs, | 61 | 54 | toUint8Array, | 62 | 55 | wrapError, | | 56 | + yieldAbortable, | 63 | 57 | } = require('internal/streams/iter/utils'); | 64 | 58 |
| 65 | 59 | const { | @@ -690,81 +684,6 @@ async function* applyValidatedStatefulAsyncTransform(source, transform, options) | 690 | 684 | options.signal?.throwIfAborted(); | 691 | 685 | } | 692 | 686 |
| 693 | | -function getOnAbort(reject, signal) { | 694 | | - return () => reject(signal.reason); | 695 | | -} | 696 | | - | 697 | | -/** | 698 | | - * Read one item from an async iterator, rejecting early if the signal aborts. | 699 | | - * @param {AsyncIterator} iterator - The iterator to read from. | 700 | | - * @param {AbortSignal|undefined} signal - Optional abort signal. | 701 | | - * @returns {Promise<IteratorResult<Uint8Array[]>>|IteratorResult<Uint8Array[]>} | 702 | | - */ | 703 | | -function abortableNext(iterator, signal) { | 704 | | - if (signal === undefined) { | 705 | | - return iterator.next(); | 706 | | - } | 707 | | - | 708 | | - signal.throwIfAborted(); | 709 | | - | 710 | | - const next = iterator.next(); | 711 | | - const { promise, reject } = PromiseWithResolvers(); | 712 | | - const onAbort = getOnAbort(reject, signal); | 713 | | - signal.addEventListener('abort', onAbort, { __proto__: null, once: true }); | 714 | | - if (signal.aborted) { | 715 | | - onAbort(); | 716 | | - } | 717 | | - | 718 | | - return SafePromisePrototypeFinally(SafePromiseRace([next, promise]), () => { | 719 | | - signal.removeEventListener('abort', onAbort); | 720 | | - }); | 721 | | -} | 722 | | - | 723 | | -/** | 724 | | - * Wrap an async source so each pending read is abort-aware. | 725 | | - * @param {AsyncIterable<Uint8Array[]>} source - The source to read from. | 726 | | - * @param {AbortSignal|undefined} signal - Optional abort signal. | 727 | | - * @returns {AsyncIterable<Uint8Array[]>} | 728 | | - */ | 729 | | -function yieldAbortable(source, signal) { | 730 | | - if (signal === undefined) { | 731 | | - return source; | 732 | | - } | 733 | | - | 734 | | - return { | 735 | | - __proto__: null, | 736 | | - async *[SymbolAsyncIterator]() { | 737 | | - const iterator = source[SymbolAsyncIterator](); | 738 | | - let completed = false; | 739 | | - let aborted = false; | 740 | | - | 741 | | - try { | 742 | | - while (true) { | 743 | | - const { done, value } = await abortableNext(iterator, signal); | 744 | | - if (done) { | 745 | | - completed = true; | 746 | | - return; | 747 | | - } | 748 | | - signal.throwIfAborted(); | 749 | | - yield value; | 750 | | - } | 751 | | - } catch (error) { | 752 | | - aborted = signal.aborted; | 753 | | - throw error; | 754 | | - } finally { | 755 | | - if (!completed && typeof iterator.return === 'function') { | 756 | | - const result = iterator.return(); | 757 | | - if (aborted) { | 758 | | - markPromiseAsHandled(result); | 759 | | - } else { | 760 | | - await result; | 761 | | - } | 762 | | - } | 763 | | - } | 764 | | - }, | 765 | | - }; | 766 | | -} | 767 | | - | 768 | 687 | /** | 769 | 688 | * Create an async pipeline from source through transforms. | 770 | 689 | * @yields {Uint8Array[]} | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/streams/iter/utils.js +84Lines changed: 84 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -8,14 +8,22 @@ const { | 8 | 8 | MathMin, | 9 | 9 | NumberMAX_SAFE_INTEGER, | 10 | 10 | PromiseResolve, | | 11 | + PromiseWithResolvers, | | 12 | + SafePromisePrototypeFinally, | | 13 | + SafePromiseRace, | 11 | 14 | String, | | 15 | + SymbolAsyncIterator, | 12 | 16 | TypedArrayPrototypeGetBuffer, | 13 | 17 | TypedArrayPrototypeGetByteLength, | 14 | 18 | TypedArrayPrototypeGetByteOffset, | 15 | 19 | TypedArrayPrototypeSet, | 16 | 20 | Uint8Array, | 17 | 21 | } = primordials; | 18 | 22 |
| | 23 | +const { | | 24 | + markPromiseAsHandled, | | 25 | +} = internalBinding('util'); | | 26 | + | 19 | 27 | const { TextEncoder } = require('internal/encoding'); | 20 | 28 | const { | 21 | 29 | codes: { | @@ -69,6 +77,81 @@ function onSignalAbort(signal, handler) { | 69 | 77 | } | 70 | 78 | } | 71 | 79 |
| | 80 | +function getOnAbort(reject, signal) { | | 81 | + return () => reject(signal.reason); | | 82 | +} | | 83 | + | | 84 | +/** | | 85 | + * Read one item from an async iterator, rejecting early if the signal aborts. | | 86 | + * @param {AsyncIterator} iterator - The iterator to read from. | | 87 | + * @param {AbortSignal|undefined} signal - Optional abort signal. | | 88 | + * @returns {Promise<IteratorResult<Uint8Array[]>>|IteratorResult<Uint8Array[]>} | | 89 | + */ | | 90 | +function abortableNext(iterator, signal) { | | 91 | + if (signal === undefined) { | | 92 | + return iterator.next(); | | 93 | + } | | 94 | + | | 95 | + signal.throwIfAborted(); | | 96 | + | | 97 | + const next = iterator.next(); | | 98 | + const { promise, reject } = PromiseWithResolvers(); | | 99 | + const onAbort = getOnAbort(reject, signal); | | 100 | + signal.addEventListener('abort', onAbort, { __proto__: null, once: true }); | | 101 | + if (signal.aborted) { | | 102 | + onAbort(); | | 103 | + } | | 104 | + | | 105 | + return SafePromisePrototypeFinally(SafePromiseRace([next, promise]), () => { | | 106 | + signal.removeEventListener('abort', onAbort); | | 107 | + }); | | 108 | +} | | 109 | + | | 110 | +/** | | 111 | + * Wrap an async source so each pending read is abort-aware. | | 112 | + * @param {AsyncIterable<Uint8Array[]>} source - The source to read from. | | 113 | + * @param {AbortSignal|undefined} signal - Optional abort signal. | | 114 | + * @returns {AsyncIterable<Uint8Array[]>} | | 115 | + */ | | 116 | +function yieldAbortable(source, signal) { | | 117 | + if (signal === undefined) { | | 118 | + return source; | | 119 | + } | | 120 | + | | 121 | + return { | | 122 | + __proto__: null, | | 123 | + async *[SymbolAsyncIterator]() { | | 124 | + const iterator = source[SymbolAsyncIterator](); | | 125 | + let completed = false; | | 126 | + let aborted = false; | | 127 | + | | 128 | + try { | | 129 | + while (true) { | | 130 | + const { done, value } = await abortableNext(iterator, signal); | | 131 | + if (done) { | | 132 | + completed = true; | | 133 | + return; | | 134 | + } | | 135 | + signal.throwIfAborted(); | | 136 | + yield value; | | 137 | + } | | 138 | + } catch (error) { | | 139 | + aborted = signal.aborted; | | 140 | + throw error; | | 141 | + } finally { | | 142 | + if (!completed && typeof iterator.return === 'function') { | | 143 | + const result = iterator.return(); | | 144 | + if (aborted) { | | 145 | + markPromiseAsHandled(result); | | 146 | + } else { | | 147 | + await result; | | 148 | + } | | 149 | + } | | 150 | + } | | 151 | + }, | | 152 | + }; | | 153 | +} | | 154 | + | 72 | 155 | /** | 73 | 156 | * Compute the minimum cursor across a set of consumers and count how many | 74 | 157 | * consumers are at that cursor. | @@ -301,4 +384,5 @@ module.exports = { | 301 | 384 | toUint8Array, | 302 | 385 | validateBackpressure, | 303 | 386 | wrapError, | | 387 | + yieldAbortable, | 304 | 388 | }; |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-stream-iter-consumers-bytes.js +52Lines changed: 52 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -14,6 +14,7 @@ const { | 14 | 14 | arrayBufferSync, | 15 | 15 | array, | 16 | 16 | arraySync, | | 17 | + toAsyncStreamable, | 17 | 18 | } = require('stream/iter'); | 18 | 19 |
| 19 | 20 | // ============================================================================= | @@ -51,6 +52,55 @@ async function testBytesAsyncAbort() { | 51 | 52 | ); | 52 | 53 | } | 53 | 54 |
| | 55 | +async function testAsyncConsumersAbortPendingNext() { | | 56 | + const consumers = [ | | 57 | + ['bytes', bytes], | | 58 | + ['text', text], | | 59 | + ['arrayBuffer', arrayBuffer], | | 60 | + ['array', array], | | 61 | + ]; | | 62 | + | | 63 | + for (const [name, consumer] of consumers) { | | 64 | + const ac = new AbortController(); | | 65 | + const reason = new Error(`${name} boom`); | | 66 | + | | 67 | + async function* never() { | | 68 | + await new Promise(() => {}); | | 69 | + yield []; | | 70 | + } | | 71 | + | | 72 | + const promise = consumer(never(), { __proto__: null, signal: ac.signal }); | | 73 | + ac.abort(reason); | | 74 | + | | 75 | + await assert.rejects(promise, reason); | | 76 | + } | | 77 | +} | | 78 | + | | 79 | +async function testAsyncConsumersAbortPendingNormalization() { | | 80 | + const consumers = [ | | 81 | + ['bytes', bytes], | | 82 | + ['text', text], | | 83 | + ['arrayBuffer', arrayBuffer], | | 84 | + ['array', array], | | 85 | + ]; | | 86 | + | | 87 | + for (const [name, consumer] of consumers) { | | 88 | + const ac = new AbortController(); | | 89 | + const reason = new Error(`${name} normalization boom`); | | 90 | + const source = { | | 91 | + __proto__: null, | | 92 | + [toAsyncStreamable]() { | | 93 | + return new Promise(() => {}); | | 94 | + }, | | 95 | + }; | | 96 | + | | 97 | + const promise = consumer(source, { __proto__: null, signal: ac.signal }); | | 98 | + ac.abort(reason); | | 99 | + | | 100 | + await assert.rejects(promise, reason); | | 101 | + } | | 102 | +} | | 103 | + | 54 | 104 | async function testBytesEmpty() { | 55 | 105 | const data = await bytes(from([])); | 56 | 106 | assert.ok(data instanceof Uint8Array); | @@ -203,6 +253,8 @@ Promise.all([ | 203 | 253 | testBytesAsync(), | 204 | 254 | testBytesAsyncLimit(), | 205 | 255 | testBytesAsyncAbort(), | | 256 | + testAsyncConsumersAbortPendingNext(), | | 257 | + testAsyncConsumersAbortPendingNormalization(), | 206 | 258 | testBytesEmpty(), | 207 | 259 | testArrayBufferSyncBasic(), | 208 | 260 | testArrayBufferAsync(), | |
|