GeezerVison is showing github.com — stream: honor AbortSignal in Writer.end() · nodejs/node@486cff4
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/broadcast.js +3-2Lines changed: 3 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -630,9 +630,10 @@ class BroadcastWriter { | 630 | 630 | return false; | 631 | 631 | } | 632 | 632 |
| 633 | | - // end() is synchronous internally - signal accepted for interface compliance. | 634 | 633 | end(options) { | 635 | | - getWriterSignal(options); | | 634 | + const signal = getWriterSignal(options); | | 635 | + if (signal?.aborted) return PromiseReject(signal.reason); | | 636 | + | 636 | 637 | if (this.#isClosed()) return this.#closed; | 637 | 638 | this.#closed = PromiseResolve(this.#totalBytes); | 638 | 639 | this.#broadcast[kEnd](); | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/streams/iter/push.js +35-3Lines changed: 35 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 8 | 8 | const { | 9 | 9 | ArrayIsArray, | 10 | 10 | ArrayPrototypePush, | | 11 | + PromisePrototypeThen, | 11 | 12 | PromiseReject, | 12 | 13 | PromiseResolve, | 13 | 14 | PromiseWithResolvers, | @@ -55,6 +56,35 @@ const { | 55 | 56 |
| 56 | 57 | const kNoFailReason = Symbol('kNoFailReason'); | 57 | 58 |
| | 59 | +function raceEndWithSignal(promise, signal) { | | 60 | + if (!signal) return promise; | | 61 | + if (signal.aborted) return PromiseReject(signal.reason); | | 62 | + | | 63 | + const { | | 64 | + promise: signaledPromise, | | 65 | + resolve, | | 66 | + reject, | | 67 | + } = PromiseWithResolvers(); | | 68 | + const onAbort = () => reject(signal.reason); | | 69 | + | | 70 | + signal.addEventListener('abort', onAbort, { | | 71 | + __proto__: null, | | 72 | + once: true, | | 73 | + }); | | 74 | + PromisePrototypeThen( | | 75 | + promise, | | 76 | + (value) => { | | 77 | + signal.removeEventListener('abort', onAbort); | | 78 | + resolve(value); | | 79 | + }, | | 80 | + (reason) => { | | 81 | + signal.removeEventListener('abort', onAbort); | | 82 | + reject(reason); | | 83 | + }, | | 84 | + ); | | 85 | + return signaledPromise; | | 86 | +} | | 87 | + | 58 | 88 | // ============================================================================= | 59 | 89 | // PushQueue - Internal Queue with Chunk-Based Backpressure | 60 | 90 | // ============================================================================= | @@ -628,7 +658,9 @@ class PushWriter { | 628 | 658 | } | 629 | 659 |
| 630 | 660 | end(options) { | 631 | | - getWriterSignal(options); | | 661 | + const signal = getWriterSignal(options); | | 662 | + if (signal?.aborted) return PromiseReject(signal.reason); | | 663 | + | 632 | 664 | const result = this.#queue.end(); | 633 | 665 | if (result === -2) { | 634 | 666 | // Errored: reject with stored error | @@ -639,11 +671,11 @@ class PushWriter { | 639 | 671 | // when consumer drains past the end sentinel | 640 | 672 | const pendingEndPromise = this.#queue.pendingEndPromise; | 641 | 673 | if (pendingEndPromise !== null) { | 642 | | - return pendingEndPromise; | | 674 | + return raceEndWithSignal(pendingEndPromise, signal); | 643 | 675 | } | 644 | 676 | const { promise, resolve, reject } = PromiseWithResolvers(); | 645 | 677 | this.#queue.setPendingEnd({ __proto__: null, promise, resolve, reject }); | 646 | | - return promise; | | 678 | + return raceEndWithSignal(promise, signal); | 647 | 679 | } | 648 | 680 | // >= 0: byte count (immediate close or idempotent) | 649 | 681 | return PromiseResolve(result); | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-stream-iter-broadcast-basic.js +17Lines changed: 17 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -111,6 +111,22 @@ async function testWriterEnd() { | 111 | 111 | assert.strictEqual(data, 'data'); | 112 | 112 | } | 113 | 113 |
| | 114 | +async function testWriterEndWithPreAbortedSignal() { | | 115 | + const { writer, broadcast: bc } = broadcast(); | | 116 | + const consumer = bc.push(); | | 117 | + const reason = new Error('end aborted'); | | 118 | + | | 119 | + await assert.rejects( | | 120 | + writer.end({ signal: AbortSignal.abort(reason) }), | | 121 | + (error) => error === reason, | | 122 | + ); | | 123 | + | | 124 | + // A rejected end must leave the writer open. | | 125 | + await writer.write('data'); | | 126 | + assert.strictEqual(await writer.end(), 4); | | 127 | + assert.strictEqual(await text(consumer), 'data'); | | 128 | +} | | 129 | + | 114 | 130 | async function testWriterFail() { | 115 | 131 | const { writer, broadcast: bc } = broadcast(); | 116 | 132 | const consumer = bc.push(); | @@ -308,6 +324,7 @@ Promise.all([ | 308 | 324 | testWriteSync(), | 309 | 325 | testWritevSync(), | 310 | 326 | testWriterEnd(), | | 327 | + testWriterEndWithPreAbortedSignal(), | 311 | 328 | testWriterFail(), | 312 | 329 | testCancelWithoutReason(), | 313 | 330 | testCancelWithReason(), | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-stream-iter-duplex.js +17Lines changed: 17 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -127,6 +127,22 @@ async function testAbortSignal() { | 127 | 127 | ); | 128 | 128 | } | 129 | 129 |
| | 130 | +async function testWriterEndWithPreAbortedSignal() { | | 131 | + const [channelA, channelB] = duplex(); | | 132 | + const reason = new Error('end aborted'); | | 133 | + | | 134 | + await assert.rejects( | | 135 | + channelA.writer.end({ signal: AbortSignal.abort(reason) }), | | 136 | + (error) => error === reason, | | 137 | + ); | | 138 | + | | 139 | + await channelA.writer.write('still open'); | | 140 | + const completedEnd = channelA.writer.end(); | | 141 | + assert.strictEqual(await text(channelB.readable), 'still open'); | | 142 | + assert.strictEqual(await completedEnd, 10); | | 143 | + await channelB.close(); | | 144 | +} | | 145 | + | 130 | 146 | async function testEmptyDuplex() { | 131 | 147 | const [channelA, channelB] = duplex(); | 132 | 148 |
| @@ -182,6 +198,7 @@ Promise.all([ | 182 | 198 | testWithOptions(), | 183 | 199 | testPerChannelOptions(), | 184 | 200 | testAbortSignal(), | | 201 | + testWriterEndWithPreAbortedSignal(), | 185 | 202 | testEmptyDuplex(), | 186 | 203 | testChannelFail(), | 187 | 204 | testAbortSignalBothChannels(), | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-stream-iter-push-writer.js +36Lines changed: 36 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -260,6 +260,40 @@ async function testEndAsyncReturnValue() { | 260 | 260 | await consume; | 261 | 261 | } | 262 | 262 |
| | 263 | +async function testEndWithPreAbortedSignal() { | | 264 | + const { writer, readable } = push(); | | 265 | + const reason = new Error('end aborted'); | | 266 | + | | 267 | + writer.writeSync('hello'); | | 268 | + await assert.rejects( | | 269 | + writer.end({ signal: AbortSignal.abort(reason) }), | | 270 | + (error) => error === reason, | | 271 | + ); | | 272 | + | | 273 | + // A rejected end must leave the writer open. | | 274 | + writer.writeSync(' world'); | | 275 | + const consume = text(readable); | | 276 | + assert.strictEqual(await writer.end(), 11); | | 277 | + assert.strictEqual(await consume, 'hello world'); | | 278 | +} | | 279 | + | | 280 | +async function testEndSignalAbortWhileDraining() { | | 281 | + const { writer, readable } = push(); | | 282 | + const controller = new AbortController(); | | 283 | + const reason = new Error('end aborted while draining'); | | 284 | + | | 285 | + writer.writeSync('hello'); | | 286 | + const abortedEnd = writer.end({ signal: controller.signal }); | | 287 | + controller.abort(reason); | | 288 | + | | 289 | + await assert.rejects(abortedEnd, (error) => error === reason); | | 290 | + | | 291 | + // Aborting the operation does not undo the end-of-stream signal. | | 292 | + const completedEnd = writer.end(); | | 293 | + assert.strictEqual(await text(readable), 'hello'); | | 294 | + assert.strictEqual(await completedEnd, 5); | | 295 | +} | | 296 | + | 263 | 297 | async function testEndAfterEndSyncWaitsForDrain() { | 264 | 298 | const { writer, readable } = push(); | 265 | 299 | writer.writeSync('hello'); | @@ -553,6 +587,8 @@ Promise.all([ | 553 | 587 | testOndrainProtocolErrorPropagates(), | 554 | 588 | testFail(), | 555 | 589 | testEndAsyncReturnValue(), | | 590 | + testEndWithPreAbortedSignal(), | | 591 | + testEndSignalAbortWhileDraining(), | 556 | 592 | testEndAfterEndSyncWaitsForDrain(), | 557 | 593 | testWriteUint8Array(), | 558 | 594 | testOndrainWaitsForDrain(), | |
|