😽
AsyncIterator.withResolvers() を for await ..of の break 対応
前の記事の改稿となります。
前のだと break すると break 後の complete の呼び出しに問題が発生していたので、その対応です。
AsyncIterator.withResolvers ??= () => {
let controller;
let closed = false;
const abortController = new AbortController();
const signal = abortController.signal;
const stream = new ReadableStream({
start(controller_) {
controller = controller_;
},
cancel(reason) {
if (!closed) {
controller.error(reason);
closed = true;
}
abortController.abort(reason);
},
});
const [values, complete] = (() => {
const values = stream.values();
const originalReturn = values.return;
// for of .. break support
values.return = complete;
return [values, complete];
function complete(value) {
if (closed) return;
controller.close();
closed = true;
abortController.abort();
return originalReturn.call(values, value);
}
})();
return {
values,
resolve: controller.enqueue.bind(controller),
complete: () => complete(),
reject: controller.error.bind(controller),
signal,
};
}
機能は次の通りです。
-
非同期イテレータの生成:
for await...of
構文で使用可能な非同期イテレータを生成します。 -
外部からの制御:
resolve
、complete
、reject
プロパティを通じて、イテレータに値を追加したり、完了させたり、エラーを発生させたりすることができます。 -
イテレータの早期終了のサポート:
for await...of
ループ内でbreak
などを使用してイテレータが早期に終了した場合でも、イテレータが適切にクリーンアップされるようにします。
戻り値のプロパティ
-
values
: 生成された非同期イテレータです。for await...of 構文で使用できます。 -
resolve
: イテレータに値を追加するための関数です。 -
complete
: イテレータを正常に完了させるための関数です。 -
reject
: イテレータにエラーを発生させるための関数です。 -
signal
: イテレータのキャンセルを監視するための AbortSignal です。
って大分複雑になってきた……。 シンプルじゃないですね。
とりあえず使い方的な サンプル置いておきます。
具体的には下記のことを行うサンプルです。
- open ボタンを押すことでダイアログを開きます
- ダイアログを開いたら value1 / value2 / value3 及び close ボタンが押せます
- value1 value2 value3 は値を送り close は閉じます
- 送られた値のうち value3の場合は 送られた側で ループを break して 処理を終了します。
- 処理が終了したのであればダイアログも閉じます
openButton.addEventListener("click", async () => {
const values = openDialog();
log("open");
for await (const i of values) {
log(`${i}`);
if (i === "value3") break;
}
log("close");
});
function openDialog() {
const controller = new AbortController();
const {
values,
resolve,
complete,
signal: innerSignal,
} = AsyncIterator.withResolvers();
const signal = AbortSignal.any([controller.signal, innerSignal]);
dialog.showModal();
// #region set value
value1.addEventListener("click", () => resolve("value1"), { signal });
value2.addEventListener("click", () => resolve("value2"), { signal });
value3.addEventListener("click", () => resolve("value3"), { signal });
// #endregion
// #region close
closeButton.addEventListener("click", () => controller.abort(), { signal });
dialog.addEventListener("clise", () => controller.abort(), { signal });
dialog.addEventListener("cancel", () => controller.abort(), { signal });
signal.addEventListener("abort", () => {
dialog.close();
complete();
});
// #endregion
return values;
}
Discussion