-
-
Save MattiasBuelens/496fc1d37adb50a733edd43853f2f60e to your computer and use it in GitHub Desktop.
/** | |
* 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; |
@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 }
(where value
is the single parameter passed to return
, 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.
let rs = new ReadableStream();
let it = rs.values();
await it.return(Promise.resolve('foo'));
// -> { done: true, value: Promise {<fulfilled>: 'foo'} }
That said, async generators do return the resolved value... 🤔
let gen = (async function*() {})();
await gen.return(Promise.resolve('foo'));
// -> { done: true, value: 'foo' }
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 };
or return { done: true, value: await value };
at the end. But keep in mind that this behavior might not be fully spec-compliant!
@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.
@MattiasBuelens when I try this in typescript, it complains about the return type of
async return(value)
, saying that the incomingvalue
is of typePromiseLike<undefined> | undefined
, whereas the expected return type is{ done: true, value: undefined}
.So is this polyfill correct in that respect? Or should it have
return { done: true, value: undefined };
at the end?