GeezerVison is showing github.com — stream: preserve falsy cancellation reasons · nodejs/node@d631e91
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 +6-4Lines changed: 6 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -88,7 +88,7 @@ class BroadcastImpl { | 88 | 88 | #consumers = new SafeSet(); | 89 | 89 | #waiters = []; // Consumers with pending resolve (subset of #consumers) | 90 | 90 | #ended = false; | 91 | | - #error = null; | | 91 | + #error; | 92 | 92 | #cancelled = false; | 93 | 93 | #options; | 94 | 94 | #writer = null; | @@ -177,7 +177,9 @@ class BroadcastImpl { | 177 | 177 | __proto__: null, | 178 | 178 | next() { | 179 | 179 | if (state.detached) { | 180 | | - if (self.#error) return PromiseReject(self.#error); | | 180 | + if (self.#error !== undefined) { | | 181 | + return PromiseReject(self.#error); | | 182 | + } | 181 | 183 | return kDone; | 182 | 184 | } | 183 | 185 |
| @@ -194,7 +196,7 @@ class BroadcastImpl { | 194 | 196 | { __proto__: null, done: false, value: chunk }); | 195 | 197 | } | 196 | 198 |
| 197 | | - if (self.#error) { | | 199 | + if (self.#error !== undefined) { | 198 | 200 | state.detached = true; | 199 | 201 | self.#deleteConsumer(state); | 200 | 202 | return PromiseReject(self.#error); | @@ -344,7 +346,7 @@ class BroadcastImpl { | 344 | 346 | } | 345 | 347 |
| 346 | 348 | [kAbort](reason) { | 347 | | - if (this.#ended || this.#error) return; | | 349 | + if (this.#ended || this.#error !== undefined) return; | 348 | 350 | this.#error = reason; | 349 | 351 | this.#ended = true; | 350 | 352 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/streams/iter/share.js +14-12Lines changed: 14 additions & 12 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -73,7 +73,7 @@ class ShareImpl { | 73 | 73 | #consumers = new SafeSet(); | 74 | 74 | #sourceIterator = null; | 75 | 75 | #sourceExhausted = false; | 76 | | - #sourceError = null; | | 76 | + #sourceError; | 77 | 77 | #cancelled = false; | 78 | 78 | #pulling = false; | 79 | 79 | #pullWaiters = []; | @@ -129,7 +129,7 @@ class ShareImpl { | 129 | 129 | __proto__: null, | 130 | 130 | [SymbolAsyncIterator]() { | 131 | 131 | const getNext = async () => { | 132 | | - if (self.#sourceError) { | | 132 | + if (self.#sourceError !== undefined) { | 133 | 133 | state.detached = true; | 134 | 134 | self.#consumers.delete(state); | 135 | 135 | throw self.#sourceError; | @@ -141,7 +141,7 @@ class ShareImpl { | 141 | 141 | // cursor must re-pull rather than terminating prematurely. | 142 | 142 | for (;;) { | 143 | 143 | if (state.detached) { | 144 | | - if (self.#sourceError) throw self.#sourceError; | | 144 | + if (self.#sourceError !== undefined) throw self.#sourceError; | 145 | 145 | return { __proto__: null, done: true, value: undefined }; | 146 | 146 | } | 147 | 147 |
| @@ -167,7 +167,7 @@ class ShareImpl { | 167 | 167 | if (self.#sourceExhausted) { | 168 | 168 | state.detached = true; | 169 | 169 | self.#deleteConsumer(state); | 170 | | - if (self.#sourceError) throw self.#sourceError; | | 170 | + if (self.#sourceError !== undefined) throw self.#sourceError; | 171 | 171 | return { __proto__: null, done: true, value: undefined }; | 172 | 172 | } | 173 | 173 |
| @@ -176,7 +176,7 @@ class ShareImpl { | 176 | 176 | if (shouldBuffer === null) { | 177 | 177 | state.detached = true; | 178 | 178 | self.#deleteConsumer(state); | 179 | | - if (self.#sourceError) throw self.#sourceError; | | 179 | + if (self.#sourceError !== undefined) throw self.#sourceError; | 180 | 180 | return { __proto__: null, done: true, value: undefined }; | 181 | 181 | } | 182 | 182 |
| @@ -260,7 +260,9 @@ class ShareImpl { | 260 | 260 |
| 261 | 261 | async #waitForBufferSpace() { | 262 | 262 | while (this.#bufferedBytes >= this.#options.budget) { | 263 | | - if (this.#cancelled || this.#sourceError || this.#sourceExhausted) { | | 263 | + if (this.#cancelled || | | 264 | + this.#sourceError !== undefined || | | 265 | + this.#sourceExhausted) { | 264 | 266 | return this.#cancelled ? null : true; | 265 | 267 | } | 266 | 268 |
| @@ -418,7 +420,7 @@ class SyncShareImpl { | 418 | 420 | #consumers = new SafeSet(); | 419 | 421 | #sourceIterator = null; | 420 | 422 | #sourceExhausted = false; | 421 | | - #sourceError = null; | | 423 | + #sourceError; | 422 | 424 | #cancelled = false; | 423 | 425 | #cachedMinCursor = 0; | 424 | 426 | #cachedMinCursorConsumers = 0; | @@ -467,14 +469,14 @@ class SyncShareImpl { | 467 | 469 | return { | 468 | 470 | __proto__: null, | 469 | 471 | next() { | 470 | | - if (state.detached) { | 471 | | - return { __proto__: null, done: true, value: undefined }; | 472 | | - } | 473 | | - if (self.#sourceError) { | | 472 | + if (self.#sourceError !== undefined) { | 474 | 473 | state.detached = true; | 475 | 474 | self.#deleteConsumer(state); | 476 | 475 | throw self.#sourceError; | 477 | 476 | } | | 477 | + if (state.detached) { | | 478 | + return { __proto__: null, done: true, value: undefined }; | | 479 | + } | 478 | 480 | if (self.#cancelled) { | 479 | 481 | state.detached = true; | 480 | 482 | self.#deleteConsumer(state); | @@ -535,7 +537,7 @@ class SyncShareImpl { | 535 | 537 |
| 536 | 538 | self.#pullFromSource(); | 537 | 539 |
| 538 | | - if (self.#sourceError) { | | 540 | + if (self.#sourceError !== undefined) { | 539 | 541 | state.detached = true; | 540 | 542 | self.#deleteConsumer(state); | 541 | 543 | throw self.#sourceError; | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-stream-iter-broadcast-basic.js +8-8Lines changed: 8 additions & 8 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -260,15 +260,15 @@ async function testWriterFailIdempotent() { | 260 | 260 | }, { message: 'fail!' }); | 261 | 261 | } | 262 | 262 |
| 263 | | -// cancel() with falsy reason (0, "", false) should still treat as error | 264 | 263 | async function testCancelWithFalsyReason() { | 265 | | - const { broadcast: bc } = broadcast(); | 266 | | - const consumer = bc.push(); | 267 | | - const resultPromise = text(consumer).catch((err) => err); | 268 | | - await new Promise((resolve) => setImmediate(resolve)); | 269 | | - bc.cancel(0); | 270 | | - const result = await resultPromise; | 271 | | - assert.strictEqual(result, 0); | | 264 | + for (const reason of [0, '', false, null]) { | | 265 | + const { broadcast: bc } = broadcast(); | | 266 | + const iterator = bc.push()[Symbol.asyncIterator](); | | 267 | + | | 268 | + bc.cancel(reason); | | 269 | + | | 270 | + await assert.rejects(iterator.next(), (error) => error === reason); | | 271 | + } | 272 | 272 | } | 273 | 273 |
| 274 | 274 | // Late-joining consumer should read from oldest buffered entry | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-stream-iter-share-async.js +12Lines changed: 12 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -132,6 +132,17 @@ async function testShareCancelWithReason() { | 132 | 132 | ); | 133 | 133 | } | 134 | 134 |
| | 135 | +async function testShareCancelWithFalsyReason() { | | 136 | + for (const reason of [0, '', false, null]) { | | 137 | + const shared = share(from('data')); | | 138 | + const iterator = shared.pull()[Symbol.asyncIterator](); | | 139 | + | | 140 | + shared.cancel(reason); | | 141 | + | | 142 | + await assert.rejects(iterator.next(), (error) => error === reason); | | 143 | + } | | 144 | +} | | 145 | + | 135 | 146 | async function testShareAbortSignal() { | 136 | 147 | const ac = new AbortController(); | 137 | 148 | const reason = new Error('share aborted'); | @@ -357,6 +368,7 @@ Promise.all([ | 357 | 368 | testShareCancel(), | 358 | 369 | testShareCancelMidIteration(), | 359 | 370 | testShareCancelWithReason(), | | 371 | + testShareCancelWithFalsyReason(), | 360 | 372 | testShareAbortSignal(), | 361 | 373 | testShareAbortSignalWhileSourcePullPending(), | 362 | 374 | testSharePullAbortSignalRejectsPendingNext(), | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-stream-iter-share-sync.js +16-20Lines changed: 16 additions & 20 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -87,37 +87,32 @@ function testShareSyncCancelMidIteration() { | 87 | 87 | } | 88 | 88 |
| 89 | 89 | function testShareSyncCancelWithReason() { | 90 | | - // When cancel(reason) is called, a consumer that hasn't started | 91 | | - // iterating is already detached, so it sees done:true (not the error). | 92 | | - // But a consumer that is mid-iteration when another consumer cancels | 93 | | - // with a reason will see the error on the next pull after cancel. | 94 | 90 | const enc = new TextEncoder(); | 95 | 91 | function* gen() { | 96 | 92 | yield [enc.encode('a')]; | 97 | 93 | yield [enc.encode('b')]; | 98 | | - yield [enc.encode('c')]; | 99 | 94 | } | 100 | 95 | const shared = shareSync(gen(), { budget: 16384 }); | 101 | | - const c1 = shared.pull(); | 102 | | - const c2 = shared.pull(); | | 96 | + const iterator1 = shared.pull()[Symbol.iterator](); | | 97 | + const iterator2 = shared.pull()[Symbol.iterator](); | | 98 | + const reason = new Error('sync cancel reason'); | | 99 | + | | 100 | + iterator1.next(); | | 101 | + shared.cancel(reason); | 103 | 102 |
| 104 | | - // c1 reads one item, then c2 cancels with a reason | 105 | | - const iter1 = c1[Symbol.iterator](); | 106 | | - const first = iter1.next(); | 107 | | - assert.strictEqual(first.done, false); | | 103 | + assert.throws(() => iterator1.next(), (error) => error === reason); | | 104 | + assert.throws(() => iterator2.next(), (error) => error === reason); | | 105 | +} | 108 | 106 |
| 109 | | - shared.cancel(new Error('sync cancel reason')); | | 107 | +function testShareSyncCancelWithFalsyReason() { | | 108 | + for (const reason of [0, '', false, null]) { | | 109 | + const shared = shareSync(fromSync('data')); | | 110 | + const iterator = shared.pull()[Symbol.iterator](); | 110 | 111 |
| 111 | | - // c1 was already iterating, it's now detached → done | 112 | | - const next = iter1.next(); | 113 | | - assert.strictEqual(next.done, true); | | 112 | + shared.cancel(reason); | 114 | 113 |
| 115 | | - // c2 never started, also detached → done (not error) | 116 | | - const batches = []; | 117 | | - for (const batch of c2) { | 118 | | - batches.push(batch); | | 114 | + assert.throws(() => iterator.next(), (error) => error === reason); | 119 | 115 | } | 120 | | - assert.strictEqual(batches.length, 0); | 121 | 116 | } | 122 | 117 |
| 123 | 118 | // ============================================================================= | @@ -157,6 +152,7 @@ Promise.all([ | 157 | 152 | testShareSyncCancel(), | 158 | 153 | testShareSyncCancelMidIteration(), | 159 | 154 | testShareSyncCancelWithReason(), | | 155 | + testShareSyncCancelWithFalsyReason(), | 160 | 156 | testShareSyncSourceError(), | 161 | 157 | testShareSyncStringSource(), | 162 | 158 | ]).then(common.mustCall()); |
|