Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save MattiasBuelens/496fc1d37adb50a733edd43853f2f60e to your computer and use it in GitHub Desktop.
Save MattiasBuelens/496fc1d37adb50a733edd43853f2f60e to your computer and use it in GitHub Desktop.
ReadableStream async iterator polyfill
/**
* 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
Copy link
Author

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

@HansBrende
Copy link

@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' } }
Screenshot 2025-08-27 at 1 54 46 PM

That's so weird though that the behavior would be inconsistent with async generators... feel like this has to be an oversight somewhere.

@HansBrende
Copy link

@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> } }

Chrome:
Screenshot 2025-08-27 at 3 45 33 PM

Firefox:
Screenshot 2025-08-27 at 3 47 14 PM

Given that information, the best typescript fix to align with existing behavior seems to be:

return { done: true, value: value as undefined };

@HansBrende
Copy link

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