Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add @@asyncIterator to ReadableStream #954

Closed
wants to merge 7 commits into from
Closed

add @@asyncIterator to ReadableStream #954

wants to merge 7 commits into from

Conversation

devsnek
Copy link

@devsnek devsnek commented Sep 7, 2018

Closes #778


Preview | Diff

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
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.

index.bs Outdated
@@ -866,6 +868,84 @@ option. If <code><a for="underlying source">type</a></code> is set to <code>unde
</code></pre>
</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
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()?

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
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).

index.bs Outdated Show resolved Hide resolved
index.bs Outdated
exception.
1. Let _result_ be ! ReadableStreamReaderGenericCancel(_reader_, _value_).
1. Otherwise,
1. Let _result_ be ! ReadableStreamCreateReadResult(_value_, *true*, *true*).
Copy link
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...

index.bs Show resolved Hide resolved
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
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
Copy link
Member

@domenic domenic left a comment

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.

Copy link
Collaborator

@MattiasBuelens MattiasBuelens left a comment

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().

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
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
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
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!

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
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
Copy link
Member

domenic commented Sep 13, 2018

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?

Copy link
Collaborator

@MattiasBuelens MattiasBuelens left a comment

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.

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
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)

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

<emu-alg>
1. If ! IsReadableStreamAsyncIterator(*this*) is *false*, throw a *TypeError* exception.
Copy link
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.

Copy link
Collaborator

@MattiasBuelens MattiasBuelens left a comment

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. 👍

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
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
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
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
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
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?

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
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
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
Copy link
Collaborator

ricea commented Sep 18, 2018

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
Copy link
Collaborator

ricea commented Sep 19, 2018

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

@devsnek
Copy link
Author

devsnek commented Sep 20, 2018

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

@ricea
Copy link
Collaborator

ricea commented Oct 4, 2018

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

@devsnek
Copy link
Author

devsnek commented Oct 4, 2018

@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
Copy link
Collaborator

ricea commented Oct 4, 2018

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

@billiegoose
Copy link

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

@ricea
Copy link
Collaborator

ricea commented Jan 22, 2019

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

@MattiasBuelens
Copy link
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
Copy link
Author

devsnek commented Jan 22, 2019

@ricea @MattiasBuelens feel free to take over

@ricea
Copy link
Collaborator

ricea commented Jan 24, 2019

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
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
Copy link
Collaborator

ricea commented Jan 24, 2019

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.

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