Skip to content

Commit 0aabdda

Browse files
committed
Fix bugs in reader releasing algorithm
As illustrated by the new tests, there were previously issues around auto-releasing not always giving the right result for errored or closed streams, especially with regard to allowing future readers to be acquired. This commit simplifies the logic and ensures consistency across all readers obtained from errored or closed streams.
1 parent f4ac4d5 commit 0aabdda

File tree

5 files changed

+129
-44
lines changed

5 files changed

+129
-44
lines changed

index.bs

+40-22
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,17 @@ Instances of <code>ReadableStreamReader</code> are created with the internal slo
539539
<tr>
540540
<td>\[[readRequests]]
541541
<td>A List of promises returned by calls to the reader's <code>read()</code> method that have not yet been resolved,
542-
due to the <a>consumer</a> requesting <a>chunks</a> sooner than they are available.
542+
due to the <a>consumer</a> requesting <a>chunks</a> sooner than they are available
543+
</tr>
544+
<tr>
545+
<td>\[[state]]
546+
<td>A string containing the reader's current state, used internally; one of <code>"readable"</code>,
547+
<code>"closed"</code>, or <code>"errored"</code>
548+
</tr>
549+
<tr>
550+
<td>\[[storedError]]
551+
<td>A value indicating how the reader's stream failed, to be given as a failure reason or exception when trying to
552+
operate on the reader; applicable only when \[[state]] is <code>"errored"</code>
543553
</tr>
544554
</table>
545555

@@ -556,12 +566,12 @@ Instances of <code>ReadableStreamReader</code> are created with the internal slo
556566
<li> If IsReadableStreamLocked(<var>stream</var>) is <b>true</b>, throw a <b>TypeError</b> exception.
557567
<li> Set <var>stream</var>@\[[reader]] to <b>this</b>.
558568
<li> Set <b>this</b>@\[[ownerReadableStream]] to <var>stream</var>.
569+
<li> Set <b>this</b>@\[[state]] to <code>"readable"</code>.
570+
<li> Set <b>this</b>@\[[storedError]] to <b>undefined</b>.
559571
<li> Set <b>this</b>@\[[readRequests]] to a new empty List.
560572
<li> Set <b>this</b>@\[[closedPromise]] to a new promise.
561-
<li> If <var>stream</var>@\[[state]] is <code>"closed"</code>, resolve <b>this</b>@\[[closedPromise]] with
562-
<b>undefined</b>.
563-
<li> If <var>stream</var>@\[[state]] is <code>"errored"</code>, reject <b>this</b>@\[[closedPromise]] with
564-
<var>stream</bar>@\[[storedError]].
573+
<li> If <var>stream</var>@\[[state]] is <code>"closed"</code> or <code>"errored"</code>, call-with-rethrow
574+
ReleaseReadableStreamReader(<b>this</b>).
565575
</ol>
566576

567577
<h4 id="reader-prototype">Properties of the <code>ReadableStreamReader</code> Prototype</h4>
@@ -614,11 +624,12 @@ Instances of <code>ReadableStreamReader</code> are created with the internal slo
614624

615625
<ol>
616626
<li> If IsReadableStreamReader(<b>this</b>) is <b>false</b>, throw a <b>TypeError</b> exception.
617-
<li> If <b>this</b>@\[[ownerReadableStream]] is <b>undefined</b> or
618-
<b>this</b>@\[[ownerReadableStream]]@\[[state]] is <code>"closed"</code>, return a new promise resolved with
627+
<li> If <b>this</b>@\[[state]] is <code>"closed"</code>, return a new promise resolved with
619628
CreateIterResultObject(<b>undefined</b>, <b>true</b>).
620-
<li> If <b>this</b>@\[[ownerReadableStream]]@\[[state]] is <code>"errored"</code>, return a new promise
621-
rejected with <b>this</b>@\[[ownerReadableStream]]@\[[storedError]].
629+
<li> If <b>this</b>@\[[state]] is <code>"errored"</code>, return a new promise rejected with
630+
<b>this</b>@\[[storedError]].
631+
<li> Assert: <b>this</b>@\[[ownerReadableStream]] is not <b>undefined</b>.
632+
<li> Assert: <b>this</b>@\[[ownerReadableStream]]@\[[state]] is <code>"readable"</code>.
622633
<li> If <b>this</b>@\[[ownerReadableStream]]@\[[queue]] is not empty,
623634
<ol>
624635
<li> Let <var>chunk</var> be DequeueValue(<b>this</b>@\[[ownerReadableStream]]@\[[queue]]).
@@ -820,16 +831,8 @@ a variable <var>stream</var>, that performs the following steps:
820831
<li> Let <var>stream</var>@\[[queue]] be a new empty List.
821832
<li> Set <var>stream</var>@\[[storedError]] to <var>e</var>.
822833
<li> Set <var>stream</var>@\[[state]] to <code>"errored"</code>.
823-
<li> If IsReadableStreamLocked(<var>stream</var>) is <b>true</b>,
824-
<ol>
825-
<li> Reject <var>stream</var>@\[[reader]]@\[[closedPromise]] with <var>e</var>.
826-
<li> Repeat for each <var>readRequestPromise</var> that is an element of
827-
<var>stream</var>@\[[reader]]@\[[readRequests]],
828-
<ol>
829-
<li> Reject <var>readRequestPromise</var> with <var>e</var>.
830-
</ol>
831-
<li> Set <var>stream</var>@\[[reader]]@\[[readRequests]] to a new empty List.
832-
</ol>
834+
<li> If IsReadableStreamLocked(<var>stream</var>) is <b>true</b>, return
835+
ReleaseReadableStreamReader(<var>stream</var>@\[[reader]]).
833836
</ol>
834837

835838
<h4 id="is-readable-stream">IsReadableStream ( x )</h4>
@@ -865,14 +868,29 @@ a variable <var>stream</var>, that performs the following steps:
865868

866869
<ol>
867870
<li> Assert: <var>reader</var>@\[[ownerReadableStream]] is not <b>undefined</b>.
868-
<li> Repeat for each <var>readRequestPromise</var> that is an element of <var>reader</var>@\[[readRequests]],
871+
<li> If <var>reader</var>@\[[ownerReadableStream]]@\[[state]] is <code>"errored"</code>,
869872
<ol>
870-
<li> Resolve <var>readRequestPromise</var> with CreateIterResultObject(<b>undefined</b>, <b>true</b>).
873+
<li> Set <var>reader</var>@\[[state]] to <code>"errored"</code>.
874+
<li> Let <var>e</var> be <var>reader</var>@\[[ownerReadableStream]]@\[[storedError]].
875+
<li> Set <var>reader</var>@\[[storedError]] to <var>e</var>.
876+
<li> Reject <var>reader</var>@\[[closedPromise]] with <var>e</var>.
877+
<li> Repeat for each <var>readRequestPromise</var> that is an element of <var>reader</var>@\[[readRequests]],
878+
<ol>
879+
<li> Reject <var>readRequestPromise</var> with <var>e</var>.
880+
</ol>
881+
</ol>
882+
<li> Otherwise,
883+
<ol>
884+
<li> Set <var>reader</var>@\[[state]] to <code>"closed"</code>.
885+
<li> Resolve <var>reader</var>@\[[closedPromise]] with <b>undefined</b>.
886+
<li> Repeat for each <var>readRequestPromise</var> that is an element of <var>reader</var>@\[[readRequests]],
887+
<ol>
888+
<li> Resolve <var>readRequestPromise</var> with CreateIterResultObject(<b>undefined</b>, <b>true</b>).
889+
</ol>
871890
</ol>
872891
<li> Set <var>reader</var>@\[[readRequests]] to a new empty List.
873892
<li> Set <var>reader</var>@\[[ownerReadableStream]]@\[[reader]] to <b>undefined</b>.
874893
<li> Set <var>reader</var>@\[[ownerReadableStream]] to <b>undefined</b>.
875-
<li> Resolve <var>reader</var>@\[[closedPromise]] with <b>undefined</b>.
876894
</ol>
877895

878896
<h4 id="should-readable-stream-apply-backpressure">ShouldReadableStreamApplyBackpressure ( stream )</h4>

reference-implementation/lib/readable-stream.js

+32-20
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ class ReadableStreamReader {
154154

155155
stream._reader = this;
156156
this._ownerReadableStream = stream;
157+
this._state = 'readable';
158+
this._storedError = undefined;
157159

158160
this._readRequests = [];
159161

@@ -162,12 +164,8 @@ class ReadableStreamReader {
162164
this._closedPromise_reject = reject;
163165
});
164166

165-
if (stream._state === 'closed') {
166-
this._closedPromise_resolve(undefined);
167-
}
168-
169-
if (stream._state === 'errored') {
170-
this._closedPromise_reject(stream._storedError);
167+
if (stream._state === 'closed' || stream._state === 'errored') {
168+
ReleaseReadableStreamReader(this);
171169
}
172170
}
173171

@@ -199,14 +197,17 @@ class ReadableStreamReader {
199197
new TypeError('ReadableStreamReader.prototype.read can only be used on a ReadableStreamReader'));
200198
}
201199

202-
if (this._ownerReadableStream === undefined || this._ownerReadableStream._state === 'closed') {
200+
if (this._state === 'closed') {
203201
return Promise.resolve(CreateIterResultObject(undefined, true));
204202
}
205203

206-
if (this._ownerReadableStream._state === 'errored') {
207-
return Promise.reject(this._ownerReadableStream._storedError);
204+
if (this._state === 'errored') {
205+
return Promise.reject(this._storedError);
208206
}
209207

208+
assert(this._ownerReadableStream !== undefined);
209+
assert(this._ownerReadableStream._state === 'readable');
210+
210211
if (this._ownerReadableStream._queue.length > 0) {
211212
const chunk = DequeueValue(this._ownerReadableStream._queue);
212213

@@ -242,7 +243,7 @@ class ReadableStreamReader {
242243
throw new TypeError('Tried to release a reader lock when that reader has pending read() calls un-settled');
243244
}
244245

245-
ReleaseReadableStreamReader(this);
246+
return ReleaseReadableStreamReader(this);
246247
}
247248
}
248249

@@ -387,12 +388,7 @@ function CreateReadableStreamErrorFunction(stream) {
387388
stream._state = 'errored';
388389

389390
if (IsReadableStreamLocked(stream) === true) {
390-
stream._reader._closedPromise_reject(e);
391-
392-
for (const { _reject } of stream._reader._readRequests) {
393-
_reject(e);
394-
}
395-
stream._reader._readRequests = [];
391+
return ReleaseReadableStreamReader(stream._reader);
396392
}
397393
};
398394
}
@@ -432,14 +428,30 @@ function IsReadableStreamReader(x) {
432428
}
433429

434430
function ReleaseReadableStreamReader(reader) {
435-
for (const { _resolve } of reader._readRequests) {
436-
_resolve(CreateIterResultObject(undefined, true));
431+
assert(reader._ownerReadableStream !== undefined);
432+
433+
if (reader._ownerReadableStream._state === 'errored') {
434+
reader._state = 'errored';
435+
436+
const e = reader._ownerReadableStream._storedError;
437+
reader._storedError = e;
438+
reader._closedPromise_reject(e);
439+
440+
for (const { _reject } of reader._readRequests) {
441+
_reject(e);
442+
}
443+
} else {
444+
reader._state = 'closed';
445+
reader._closedPromise_resolve(undefined);
446+
447+
for (const { _resolve } of reader._readRequests) {
448+
_resolve(CreateIterResultObject(undefined, true));
449+
}
437450
}
438-
reader._readRequests = [];
439451

452+
reader._readRequests = [];
440453
reader._ownerReadableStream._reader = undefined;
441454
reader._ownerReadableStream = undefined;
442-
reader._closedPromise_resolve(undefined);
443455
}
444456

445457
function ShouldReadableStreamApplyBackpressure(stream) {

reference-implementation/test/readable-stream-reader.js

+34
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,37 @@ test('cancel() on a released reader is a no-op and does not pass through', t =>
198198

199199
setTimeout(() => t.end(), 50);
200200
});
201+
202+
test('Getting a second reader after erroring the stream should succeed', t => {
203+
t.plan(5);
204+
205+
let doError;
206+
const theError = new Error('bad');
207+
const rs = new ReadableStream({
208+
start(enqueue, close, error) {
209+
doError = error;
210+
}
211+
});
212+
213+
const reader1 = rs.getReader();
214+
215+
reader1.closed.catch(e => {
216+
t.equal(e, theError, 'the first reader closed getter should be rejected with the error');
217+
});
218+
219+
reader1.read().catch(e => {
220+
t.equal(e, theError, 'the first reader read() should be rejected with the error');
221+
});
222+
223+
t.throws(() => rs.getReader(), /TypeError/, 'trying to get another reader before erroring should throw');
224+
225+
doError(theError);
226+
227+
rs.getReader().closed.catch(e => {
228+
t.equal(e, theError, 'the second reader closed getter should be rejected with the error');
229+
});
230+
231+
rs.getReader().read().catch(e => {
232+
t.equal(e, theError, 'the third reader read() should be rejected with the error');
233+
});
234+
});

reference-implementation/test/templated/readable-stream-closed.js

+13-2
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,21 @@ export default (label, factory) => {
4747
startPromise.then(() => {
4848
t.equal(ws.state, 'writable', 'writable stream should start in writable state');
4949

50-
rs.pipeTo(ws).then(() => {
50+
return rs.pipeTo(ws).then(() => {
5151
t.pass('pipeTo promise should be fulfilled');
5252
t.equal(ws.state, 'closed', 'writable stream should become closed');
5353
});
54-
});
54+
})
55+
.catch(e => t.error(e));
56+
});
57+
58+
test('should be able to acquire multiple readers, since they are all auto-released', t => {
59+
const rs = factory();
60+
61+
rs.getReader();
62+
63+
t.doesNotThrow(() => rs.getReader(), 'getting a second reader should not throw');
64+
t.doesNotThrow(() => rs.getReader(), 'getting a third reader should not throw');
65+
t.end();
5566
});
5667
};

reference-implementation/test/templated/readable-stream-errored-sync-only.js

+10
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,14 @@ export default (label, factory, error) => {
1616
cancelPromise2.catch(e => t.equal(e, error, 'second cancel() call should reject with the error'));
1717
t.notEqual(cancelPromise1, cancelPromise2, 'cancel() calls should return distinct promises');
1818
});
19+
20+
test('should be able to acquire multiple readers, since they are all auto-released', t => {
21+
const rs = factory();
22+
23+
rs.getReader();
24+
25+
t.doesNotThrow(() => rs.getReader(), 'getting a second reader should not throw');
26+
t.doesNotThrow(() => rs.getReader(), 'getting a third reader should not throw');
27+
t.end();
28+
});
1929
};

0 commit comments

Comments
 (0)