Skip to content

add @@asyncIterator to ReadableStream - #954

Closed
devsnek wants to merge 7 commits into
whatwg:masterfrom
devsnek:feature/rs-asynciterator
Closed

devsnek wants to merge 7 commits into
whatwg:masterfrom
devsnek:feature/rs-asynciterator

Conversation

@devsnek

@devsnek devsnek commented Sep 7, 2018 •

Copy link
Copy Markdown

Closes #778


Preview | Diff

Comment thread index.bs Outdated

This functionality is especially useful for creating abstractions that desire the ability to consume a stream in its
entirety. By getting a reader for the stream, you can ensure nobody else can interleave reads with yours or cancel
the stream, which would interfere with your abstraction.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This copy-pasted text seems not as useful as the old text.

Comment thread index.bs Outdated
</div>

<!-- Bikeshed doesn't let us mark this up correctly: https://github.com/tabatkins/bikeshed/issues/1344 -->
<h5 id="rs-asynciterator" for="ReadableStream">[@@asyncIterator]({ <var>preventCancel</var> = false } = {})</h5>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happened to getIterator()?

Comment thread index.bs Outdated
lt="ReadableStreamAsyncIteratorPrototype">ReadableStreamAsyncIteratorPrototype</h3>

{{ReadableStreamAsyncIteratorPrototype}} is an ordinary object that is used by {{ReadableStream/[@@asyncIterator]()}} to
construct the objects it returns. Instances of {{ReadableStreamAsyncIteratorPrototype}} implmenet the {{AsyncIterator}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo "implement" (maybe my fault).

Comment thread index.bs Resolved
Comment thread index.bs Outdated
exception.
1. Let _result_ be ! ReadableStreamReaderGenericCancel(_reader_, _value_).
1. Otherwise,
1. Let _result_ be ! ReadableStreamCreateReadResult(_value_, *true*, *true*).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be a promise resolved with...

Comment thread index.bs Resolved
Comment thread index.bs Outdated
1. Otherwise,
1. Let _result_ be _value_.
1. Perform ! ReadableStreamReaderGenericRelease(_reader_).
1. Return <a>a promise resolved with</a> ! ReadableStreamCreateReadResult(_result_, *true*, *true*).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So now if preventCancel is false return() will give a promise for { value: promise for undefined, done: true }. Whereas if it is true, return() will give a promise for { value: value, done: true }. It should give the latter in both cases.

domenic
domenic previously approved these changes Sep 7, 2018

@domenic domenic left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! Just needs reference implementation and tests. Also good to get @ricea's sign-off, of course.

@MattiasBuelens MattiasBuelens left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still some holes left in return().

Comment thread index.bs
1. Perform ! ReadableStreamReaderGenericRelease(_reader_).
1. Return the result of <a>transforming</a> _result_ by a fulfillment handler that returns !
ReadableStreamCreateReadResult(_value_, *true*, *true*).
1. Perform ! ReadableStreamReaderGenericRelease(_reader_).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should reject if reader.[[ownerReadableStream]] is undefined. Otherwise, we may fail an assert in ReadableStreamReaderGenericRelease.

We should probably just move step 3a to before step 3.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's impossible for it to be undefined. User code would have to get access to [[reader]] to call releaseLock(), and it can't. So this should be an assert.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not impossible for it to be undefined, but you really have to go out of your way to break it:

const iterator = readable.getIterator();
iterator.return(); // releases lock
iterator.return(); // boom!

Comment thread index.bs
1. Perform ! ReadableStreamReaderGenericRelease(_reader_).
1. Return the result of <a>transforming</a> _result_ by a fulfillment handler that returns !
ReadableStreamCreateReadResult(_value_, *true*, *true*).
1. Perform ! ReadableStreamReaderGenericRelease(_reader_).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when there are pending read requests? See previous discussion.

Normally, ReadableStreamDefaultReader.releaseLock() checks whether this.[[readRequests]] is empty. However, this implementation uses ReadableStreamReaderGenericRelease directly without going through the same checks as releaseLock.

@domenic
domenic dismissed their stale review September 13, 2018 18:15

Mattias found problems I did not, oops

@domenic

domenic commented Sep 13, 2018

Copy link
Copy Markdown
Member

Let's work on a test plan together to guide @devsnek. Here's a start:

  • Basic test of a stream with a few chunks that closes
    • Using a "push" underlying source (calls c.enqueue() in start) that enqueues all at once
    • Using a "push" underlying source where you call c.enqueue() "just in time" (i.e. inside the loop body)
    • Using a "pull" underlying source (calls c.enqueue() in pull) using recordingReadableStream to verify the correct underlying source method calls
  • Degenerate streams
    • Async-iterating an errored stream throws
    • Async-iterating a closed stream never executes the loop body, but works fine
    • Async-iterating an empty but not closed/errored stream never executes the loop body and stalls the async function (end the test if 50 ms pass without the promise resolving)
  • @@asyncIterator() method is === to getIterator() method
  • Cancelation behavior (use recordingReadableStream)
    • Manually calling return() causes a cancel
    • throwing inside the loop body causes a cancel
    • breaking inside the loop body causes a cancel
    • returning inside the loop body causes a cancel
    • All of the above, but with preventCancel: true and the pass conditions reversed
  • Manual manipulation
  • Interaction with existing infrastructure
    • getIterator/@@asynciterator throw if there's already a lock
    • Monkey-patch getReader and default reader's prototype methods and ensure this does not interfere with async iteration (e.g. using one of the basic test cases)
    • Basic test with a few chunks but you've already consumed one or two via a normal reader (which you then released)
  • Auto-release
    • You can still acquire a reader and successfully use its .closed promise after exhausting the async iterator via for-await-of
    • You can still acquire a reader and successfully use its .closed promise after return()ing from the async iterator (either manually or via break; take your pick)

Can anyone think of something else that might be missing?

@MattiasBuelens MattiasBuelens left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing we missed in the spec text: next() and return() should return rejected promises instead of throwing. @domenic got it right in their proposed test plan.

Comment thread index.bs Outdated
<h4 id="rs-asynciterator-prototype-next" method for="ReadableStreamAsyncIteratorPrototype">next()</h4>

<emu-alg>
1. If ! IsReadableStreamAsyncIterator(*this*) is *false*, throw a *TypeError* exception.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should return a rejected promise, rather than throw an exception. From this comment by @domenic:

Calling next() or return() on non-ReadableStreamDefaultReaderAsyncIterator instances rejects with TypeError (there are existing brand check tests you can add to)

Comment thread index.bs Outdated
for="ReadableStreamAsyncIteratorPrototype">return( <var>value</var> )</h4>

<emu-alg>
1. If ! IsReadableStreamAsyncIterator(*this*) is *false*, throw a *TypeError* exception.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, this should return a rejected promise.

@MattiasBuelens MattiasBuelens left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One tiny typo left. Other than that, spec text looks good. πŸ‘

Comment thread index.bs Outdated
<h4 id="rs-asynciterator-prototype-next" method for="ReadableStreamAsyncIteratorPrototype">next()</h4>

<emu-alg>
1. If ! IsReadableStreamAsyncIterator(*this*) is *false*, <a>a promise rejected with</a> *TypeError* exception.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The word "return" is missing in this sentence.

return Promise.reject(streamAsyncIteratorBrandCheckException('next'));
}
const reader = this._asyncIteratorReader;
if (this.preventCancel === false) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be this._preventCancel.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! This would have been an embarrassing bug. πŸ˜›

}
const reader = this._asyncIteratorReader;
if (reader._ownerReadableStream === undefined) {
return Promise.reject(readerLockException('next'));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be readerLockException('iterate')?

"Cannot next a stream using a released reader" doesn't make much sense.

const reader = this._asyncIteratorReader;
if (this.preventCancel === false) {
if (reader._ownerReadableStream === undefined) {
return Promise.reject(readerLockException('next'));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be 'next'. Maybe 'finish iterating' makes sense?

Comment thread index.bs Outdated
1. If ! IsReadableStreamAsyncIterator(*this*) is *false*, return <a>a promise rejected with</a> *TypeError* exception.
1. Let _reader_ be *this*.[[asyncIteratorReader]].
1. If *this*.[[preventCancel]] is *false*, then:
1. If _reader_.[[ownerReadableStream]] is *undefined*, return <a>a promise rejected with</a> a *TypeError*

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two steps are in both branches of the if statement and so should be moved up above it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also be changed in the reference implementation.

@ricea

ricea commented Sep 18, 2018

Copy link
Copy Markdown
Collaborator

Can anyone think of something else that might be missing?

We also need a test to verify that it inherits from %AsyncIteratorPrototype%. I don't know the best way to do that.

@ricea

ricea commented Sep 19, 2018

Copy link
Copy Markdown
Collaborator

Looks good. Are you working on some web platform tests?

@devsnek

devsnek commented Sep 20, 2018

Copy link
Copy Markdown
Author

@ricea yeah, its coming along. i'll have a pr up this weekend hopefully.

@ricea

ricea commented Oct 4, 2018

Copy link
Copy Markdown
Collaborator

@devsnek How is it going? Could you upload a work-in-progress PR of the tests so I can help out?

@devsnek

devsnek commented Oct 4, 2018

Copy link
Copy Markdown
Author

@ricea sorry its been taking me so long to finish this up. here's what i've done so far: web-platform-tests/wpt#13362

@ricea

ricea commented Oct 4, 2018

Copy link
Copy Markdown
Collaborator

@devsnek Thanks, I had a quick look and it looks good. I will pick through it in more detail tomorrow.

@billiegoose

Copy link
Copy Markdown

I'm really excited about this! Looking forward to it landing. 🍻

@ricea

ricea commented Jan 22, 2019

Copy link
Copy Markdown
Collaborator

@devsnek I can take this over if you are busy. Please let me know.

@MattiasBuelens

Copy link
Copy Markdown
Collaborator

Oh right, I previously said that I could also pick this up in my free time if needed. But if you want to take it over, that's also fine by me, then I'll help with the review instead. πŸ™‚

@devsnek

devsnek commented Jan 22, 2019

Copy link
Copy Markdown
Author

@ricea @MattiasBuelens feel free to take over

@ricea

ricea commented Jan 24, 2019

Copy link
Copy Markdown
Collaborator

Oh right, I previously said that I could also pick this up in my free time if needed. But if you want to take it over, that's also fine by me, then I'll help with the review instead.

Okay, well since you offered first, you should have first opportunity to do it. If you're busy I'll pick it up in a week or so.

@MattiasBuelens

Copy link
Copy Markdown
Collaborator

Okay, well since you offered first, you should have first opportunity to do it. If you're busy I'll pick it up in a week or so.

Thanks! πŸ˜„ I think I can look into it over the weekend.

Just to check: I should branch off from web-platform-tests/wpt#13362, and then submit a new PR, right? Since I obviously can't push commits to a branch I don't own. And if further changes are needed on the spec or reference implementation, those can also go into a new PR?

@ricea

ricea commented Jan 24, 2019

Copy link
Copy Markdown
Collaborator

Just to check: I should branch off from web-platform-tests/wpt#13362, and then submit a new PR, right? Since I obviously can't push commits to a branch I don't own. And if further changes are needed on the spec or reference implementation, those can also go into a new PR?

Sounds good to me.

@devsnek devsnek closed this Jan 26, 2019
@devsnek
devsnek deleted the feature/rs-asynciterator branch July 8, 2021 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

5 participants