ENCA 5日目: AsyncFromSyncIterator で yield リジェクト時に元のイテレーターを閉じる(進行中)
非同期イテラブル(非同期反復可能)とは
ES2018 から仕様の中に非同期イテラブルインターフェースと非同期イテレーターインターフェースが定義されています。非同期イテラブルインターフェースを実装したオブジェクト[1]のことを単に非同期イテラブル(非同期反復可能)と呼びます。
ざっくり TypeScript の型で表現すると以下のようになります(実際の TypeScript での型はジェネリクスになっています)。
interface AsyncIterable {
[Symbol.asyncIerator](): AsyncIterator;
}
interface AsyncIterator {
next(value?: any): Promise<IteratorResult>;
return?(value?: any): Promise<IteratorResult>;
throw?(error?: any): Promise<IteratorResult>;
}
interface IteratorResult {
done: boolean;
value: any;
}
非同期イテラブルに対しては for await...of や非同期ジェネレーター函数内で yield *
などのシンタックスを使うことが出来ます。
詳しくは過去の記事を参考にしてください。
非同期イテラブルを使った例はあまりありませんが、例えば Web 標準の ReadableStream
が非同期イテラブルです(2024年12月現在、JavaScriptCore で実装されていないようです)。
yield
の違い
ジェネレーター函数と非同期ジェネレーター函数の ジェネレーター函数と非同期ジェネレーター函数では yield
の扱いが異なります。Promise
を yield
する場合、ジェネレーター函数の場合はそのままの Promise
が渡りますが、非同期ジェネレーター函数の場合はその中身が渡ります。
function* generatorFunction() {
yield Promise.resolve(1);
yield Promise.resolve(2);
yield Promise.resolve(3);
}
const generator = generatorFunction();
for (const value of generator) {
console.log(value); // Promise { 1 }, Promise { 2 }, Promise { 3 }
}
async function* asyncGeneratorFunction() {
yield Promise.resolve(1);
yield Promise.resolve(2);
yield Promise.resolve(3);
}
const asyncGenerator = asyncGeneratorFunction();
for await (const value of asyncGenerator) {
console.log(value); // 1, 2, 3
}
さてここで拒否された(rejected)Promise
を yield
してみましょう。非同期ジェネレーター函数の方では例外が発生していることがわかります。
function* generatorFunction() {
yield Promise.reject(1);
yield Promise.reject(2);
yield Promise.reject(3);
}
const generator = generatorFunction();
for (const value of generator) {
console.log(value); // Promise { <rejected> 1 }, Promise { <rejected> 2 }, Promise { <rejected> 3 }
}
async function* asyncGeneratorFunction() {
yield Promise.reject(1);
yield Promise.reject(2);
yield Promise.reject(3);
}
const asyncGenerator = asyncGeneratorFunction();
try {
// 例外が発生!
for await (const value of asyncGenerator);
} catch (e) {
console.log(e); // 1
}
AsyncFromSyncIterator
で yield
リジェクト時に元のイテレーターを閉じる
イテラブルに対して非同期イテラブル用のシンタックスである for await...of や非同期ジェネレーター函数内で yield *
を使うと、内部的には AsyncFromSyncIterator
でラップされることとなります。そして非同期ジェネレーター函数の yield
の挙動と同じように、拒否された Promise
に対して例外が発生するようになります。
function* generatorFunction() {
yield Promise.reject(1);
yield Promise.reject(2);
yield Promise.reject(3);
}
const generator = generatorFunction();
try {
// 例外が発生!
for await (const value of generator);
} catch (e) {
console.log(e); // 1
}
さてこのケースで元のイテレーターが閉じられていない問題が発覚しました。
function* generatorFunction() {
yield Promise.reject(1);
yield Promise.reject(2);
yield Promise.reject(3);
}
const generator = generatorFunction();
try {
// 例外が発生!
for await (const value of generator);
} catch (e) {
console.log(e); // 1
}
for (const value of generator) {
// イテレーターが閉じられていない!
console.log(value); // Promise { <rejected> 2 }, Promise { <rejected> 3 }
}
この問題は2021年1月の会議でちゃんと元のイテレーターを閉じる修正が承認されましたが、SpiderMonkey でまだ実装が完了しておらず、仕様への反映もまだのようです。
-
イテラブル同様、プリミティブもプロトタイプ汚染で無理矢理非同期イテラブルにすることは出来ますが、そんなことをする方はいないですよね。 ↩︎
Discussion