😽

AsyncIterator.withResolvers() を for await ..of の break 対応

2025/03/07に公開

前の記事の改稿となります。

https://zenn.dev/juners/articles/0c575b02f4c5e8

前のだと 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,
  };
}

機能は次の通りです。

  1. 非同期イテレータの生成: for await...of 構文で使用可能な非同期イテレータを生成します。
  2. 外部からの制御: resolvecompletereject プロパティを通じて、イテレータに値を追加したり、完了させたり、エラーを発生させたりすることができます。
  3. イテレータの早期終了のサポート: for await...of ループ内で break などを使用してイテレータが早期に終了した場合でも、イテレータが適切にクリーンアップされるようにします。

戻り値のプロパティ

  • values: 生成された非同期イテレータです。for await...of 構文で使用できます。
  • resolve: イテレータに値を追加するための関数です。
  • complete: イテレータを正常に完了させるための関数です。
  • reject: イテレータにエラーを発生させるための関数です。
  • signal: イテレータのキャンセルを監視するための AbortSignal です。

って大分複雑になってきた……。 シンプルじゃないですね。

とりあえず使い方的な サンプル置いておきます。

具体的には下記のことを行うサンプルです。

  1. open ボタンを押すことでダイアログを開きます
  2. ダイアログを開いたら value1 / value2 / value3 及び close ボタンが押せます
  3. value1 value2 value3 は値を送り close は閉じます
  4. 送られた値のうち value3の場合は 送られた側で ループを break して 処理を終了します。
  5. 処理が終了したのであればダイアログも閉じます
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