Created
September 1, 2023 09:17
-
-
Save MattiasBuelens/496fc1d37adb50a733edd43853f2f60e to your computer and use it in GitHub Desktop.
ReadableStream async iterator polyfill
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A polyfill for `ReadableStream.protototype[Symbol.asyncIterator]`, | |
* aligning as closely as possible to the specification. | |
* | |
* @see https://streams.spec.whatwg.org/#rs-asynciterator | |
* @see https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream#async_iteration | |
*/ | |
ReadableStream.prototype.values ??= function({ preventCancel = false } = {}) { | |
const reader = this.getReader(); | |
return { | |
async next() { | |
try { | |
const result = await reader.read(); | |
if (result.done) { | |
reader.releaseLock(); | |
} | |
return result; | |
} catch (e) { | |
reader.releaseLock(); | |
throw e; | |
} | |
}, | |
async return(value) { | |
if (!preventCancel) { | |
const cancelPromise = reader.cancel(value); | |
reader.releaseLock(); | |
await cancelPromise; | |
} else { | |
reader.releaseLock(); | |
} | |
return { done: true, value }; | |
}, | |
[Symbol.asyncIterator]() { | |
return this; | |
} | |
}; | |
}; | |
ReadableStream.prototype[Symbol.asyncIterator] ??= ReadableStream.prototype.values; |
@MattiasBuelens good info. Just checked node and firefox to be sure, and the behavior is consistent with chrome:
node -e "let rs = new ReadableStream(); let it = rs.values(); it.return(Promise.resolve('foo')).then(x => console.log(x));"
{ done: true, value: Promise { 'foo' } }

That's so weird though that the behavior would be inconsistent with async generators... feel like this has to be an oversight somewhere.
@MattiasBuelens important note though: it does not appear as though the incoming promise is awaited:
node -e "let rs = new ReadableStream(); let it = rs.values(); it.return(new Promise(setTimeout)).then(console.log);"
{ done: true, value: Promise { <pending> } }
Given that information, the best typescript fix to align with existing behavior seems to be:
return { done: true, value: value as undefined };
I've subsequently posted an issue in the WebIDL github to confirm whether this behavior is intentional or an oversight: whatwg/webidl#1522.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@HansBrende Hmm, there's something weird.
The WebIDL specification says in step 12 that the async iterator
return()
method must resolve with{ done: true, value: value }
(wherevalue
is the single parameter passed toreturn
, if any). However, the TypeScript type definitions seem to suggest that if you pass a promise as first parameter, then the method should resolve with the resolved value of that promise. 🤔I think the TypeScript definitions are wrong. If I try this snippet in Chrome, I get a
{ done: true, value: Promise }
result.That said, async generators do return the resolved value... 🤔
Uhm, not sure yet. Maybe this is an oversight in the WebIDL specification? 😅
If you just want to fix the types, you can do
return { done: true, value: undefined };
orreturn { done: true, value: await value };
at the end. But keep in mind that this behavior might not be fully spec-compliant!