Skip to content

Commit 8a7d92b

Browse files
Resolve BYOB reads immediately on cancel
Previously, if you cancel the stream while a BYOB read is pending, the stream has to wait for the underlying byte source to call respond(0) before it can return the BYOB request's buffer to the caller. This makes underlying byte sources difficult to write in a robust way. After this change, the contract changes so that canceling a stream while a BYOB read is pending will always lead to the stream not giving you back your buffer. This means that cancel() can immediately resolve all pending BYOB reads with the classic { done: true, value: undefined }, without having to wait for the underlying byte source. This solves the problem, and would make it easier to implement an underlying byte source. To make this work, an additional change was required: when the stream is canceled, any pending BYOB request is now immediately invalidated, so the underlying byte source doesn't erroneously think that it still needs to provide a response. (This aligns with the behavior of controller.enqueue(), which throws if the stream is already closed.) See #1114 (comment) and #1123 (comment) for some discussion and background.
1 parent 033c6d9 commit 8a7d92b

File tree

4 files changed

+27
-13
lines changed

4 files changed

+27
-13
lines changed

index.bs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,17 +1322,19 @@ the following [=struct/items=]:
13221322
: <dfn export for="read-into request">chunk steps</dfn>
13231323
:: An algorithm taking a [=chunk=], called when a chunk is available for reading
13241324
: <dfn export for="read-into request">close steps</dfn>
1325-
:: An algorithm taking a [=chunk=], called when no chunks are available because the stream is
1326-
closed
1325+
:: An algorithm taking a [=chunk=] or undefined, called when no chunks are available because
1326+
the stream is closed
13271327
: <dfn export for="read-into request">error steps</dfn>
13281328
:: An algorithm taking a JavaScript value, called when no [=chunks=] are available because the
13291329
stream is errored
13301330

13311331
<p class="note">The [=read-into request/close steps=] take a [=chunk=] so that it can return the
13321332
backing memory to the caller if possible. For example,
13331333
{{ReadableStreamBYOBReader/read()|byobReader.read(chunk)}} will fulfill with <code highlight="js">{
1334-
value: newViewOnSameMemory, done: true }</code> for closed streams, instead of the more traditional
1335-
<code highlight="js">{ value: undefined, done: true }</code>.
1334+
value: newViewOnSameMemory, done: true }</code> for closed streams. If the stream is
1335+
[=cancel a readable stream|canceled=], the backing memory is discarded and
1336+
{{ReadableStreamBYOBReader/read()|byobReader.read(chunk)}} fulfills with the more traditional
1337+
<code highlight="js">{ value: undefined, done: true }</code> instead.
13361338

13371339
<h4 id="byob-reader-prototype">Constructor, methods, and properties</h4>
13381340

@@ -1369,6 +1371,10 @@ value: newViewOnSameMemory, done: true }</code> for closed streams, instead of t
13691371
the same type) onto the same backing memory region, with no modifications, to ensure the memory
13701372
is returned to the caller.
13711373

1374+
<li>If the reader is [=cancel a readable stream|canceled=], the promise will be fulfilled with
1375+
an object of the form <code highlight="js">{ value: undefined, done: true }</code>. In this case,
1376+
the backing memory region of |view| is discarded and not returned to the caller.
1377+
13721378
<li>If the stream becomes errored, the promise will be rejected with the relevant error.
13731379
</ul>
13741380

@@ -1849,10 +1855,7 @@ counterparts for default controllers, as discussed in [[#rs-abstract-ops-used-by
18491855
id="rbs-controller-private-cancel">\[[CancelSteps]](|reason|)</dfn> implements the
18501856
[$ReadableStreamController/[[CancelSteps]]$] contract. It performs the following steps:
18511857

1852-
1. If [=this=].[=ReadableByteStreamController/[[pendingPullIntos]]=] is not [=list/is
1853-
empty|empty=],
1854-
1. Let |firstDescriptor| be [=this=].[=ReadableByteStreamController/[[pendingPullIntos]]=][0].
1855-
1. Set |firstDescriptor|'s [=pull-into descriptor/bytes filled=] to 0.
1858+
1. Perform ! [$ReadableByteStreamControllerClearPendingPullIntos$]([=this=]).
18561859
1. Perform ! [$ResetQueue$]([=this=]).
18571860
1. Let |result| be the result of performing
18581861
[=this=].[=ReadableByteStreamController/[[cancelAlgorithm]]=], passing in |reason|.
@@ -2361,6 +2364,11 @@ the {{ReadableStream}}'s public API.
23612364
1. If |stream|.[=ReadableStream/[[state]]=] is "`errored`", return [=a promise rejected with=]
23622365
|stream|.[=ReadableStream/[[storedError]]=].
23632366
1. Perform ! [$ReadableStreamClose$](|stream|).
2367+
1. Let |reader| be |stream|.[=ReadableStream/[[reader]]=].
2368+
1. If |reader| is not undefined and |reader| [=implements=] {{ReadableStreamBYOBReader}},
2369+
1. [=list/For each=] |readIntoRequest| of |reader|.[=ReadableStreamBYOBReader/[[readIntoRequests]]=],
2370+
1. Perform |readIntoRequest|'s [=read-into request/close steps=], given undefined.
2371+
1. Set |reader|.[=ReadableStreamBYOBReader/[[readIntoRequests]]=] to an empty [=list=].
23642372
1. Let |sourceCancelPromise| be !
23652373
|stream|.[=ReadableStream/[[controller]]=].[$ReadableStreamController/[[CancelSteps]]$](|reason|).
23662374
1. Return the result of [=reacting=] to |sourceCancelPromise| with a fulfillment step that returns

reference-implementation/lib/ReadableByteStreamController-impl.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,7 @@ exports.implementation = class ReadableByteStreamControllerImpl {
6666
}
6767

6868
[CancelSteps](reason) {
69-
if (this._pendingPullIntos.length > 0) {
70-
const firstDescriptor = this._pendingPullIntos[0];
71-
firstDescriptor.bytesFilled = 0;
72-
}
69+
aos.ReadableByteStreamControllerClearPendingPullIntos(this);
7370

7471
ResetQueue(this);
7572

reference-implementation/lib/abstract-ops/readable-streams.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Object.assign(exports, {
2828
IsReadableStreamLocked,
2929
ReadableByteStreamControllerCallPullIfNeeded,
3030
ReadableByteStreamControllerClearAlgorithms,
31+
ReadableByteStreamControllerClearPendingPullIntos,
3132
ReadableByteStreamControllerClose,
3233
ReadableByteStreamControllerEnqueue,
3334
ReadableByteStreamControllerError,
@@ -453,6 +454,14 @@ function ReadableStreamCancel(stream, reason) {
453454

454455
ReadableStreamClose(stream);
455456

457+
const reader = stream._reader;
458+
if (reader !== undefined && ReadableStreamBYOBReader.isImpl(reader)) {
459+
for (const readIntoRequest of reader._readIntoRequests) {
460+
readIntoRequest.closeSteps(undefined);
461+
}
462+
reader._readIntoRequests = [];
463+
}
464+
456465
const sourceCancelPromise = stream._controller[CancelSteps](reason);
457466
return transformPromiseWith(sourceCancelPromise, () => undefined);
458467
}

0 commit comments

Comments
 (0)