ポーリング処理でもawaitが使いたい

async/await、便利ですよね。
Javascript(Typescript)を利用していたら当然のようにお世話になっていることと思います。

今回はポーリング処理の終了をawaitで待ちたいというお話です。
何個か実装方法を思いついたのですが、どうにも悩んだ割に汎用性がなさそうな気がしてるので、記事にすることで供養したいと思います。

## やりたいこと

状況を整理して、やりたいことを明確にします。
処理の流れは下図の通りです。
今回は4,6の部分をPromiseに置き換え、awaitすることで4,6,7の処理が手続き的に書けるようにしたいと思います。
満たすべき要件は
1. ポーリング完了条件を満たしたらresolveするPromiseを返却すること(サーバー側での処理失敗の場合については、完了パターンができれば容易に実装できるので割愛)
1. ポーリング終了時やページ遷移時など、必要なタイミングで外側からポーリングをキャンセルできること(裏で無限にsetIntervalが動き続けないようにする)


```mermaid
sequenceDiagram
    クライアント ->> サーバー: 1. 何らかの時間のかかる処理をリクエスト
    サーバー -->> クライアント: 2. リクエスト受付レスポンス
    サーバー ->> サーバー: 3. 時間のかかる処理
    loop 処理中はループ
        クライアント ->> サーバー: 4. 処理状況の確認
        サーバー -->> クライアント: 5. 処理状況を返却
    end
    クライアント ->> クライアント: 6. 処理完了or失敗でポーリング終了
    クライアント ->> クライアント: 7. 何らかの後続処理
```

## 1. まず思いつくのは、、、

async/awaitとか使わずにcallbackで何とかするパターン。(1の要件をいきなり満たしてないですが、一番最初に思いつくやつなので一応書きます)

```typescript
function startPolling(callback: () => boolean, interval: number) {
  const intervalId = setInterval(() => {
    const isDone = callback();
    if (isDone) {
      clearInterval(intervalId);
    }
  }, interval);
  return intervalId;
}
```

clearIntervalをsetIntervalの中で呼んでて何だか気持ち悪いですよね。
ポーリング終了後の後続処理もこのままだとcallbackの中でif分岐して実施する必要があります。
外側からポーリング終了を検知して後続処理を実施するにはもう一声頑張る必要がありそうです。


## 2. Promiseを使って書き換えてみる

```typescript
function usePolling() {
  let intervalId: NodeJS.Timer;
  const start = (callback: () => boolean, interval: number) => {
    return new Promise((resolve) => {
      intervalId = setInterval(() => {
        console.log("intervalのcallback実行");
        const isDone = callback();
        if (isDone) {
          console.log("完了条件を満たしたので、ポーリング終了します");
          cancel();
          resolve("Done");
        }
      }, interval);
    });
  };

  const cancel = () => {
    clearInterval(intervalId);
  };
  return { start, cancel };
}

// 使う側
const polling = usePolling();
await polling.start(() => true, 1000);

```

少し良くなった気がします。(これで十分な気もする)
ポーリング終了を待ちたい場合は、returnされたPromiseをawaitすればよいですし、returnされたcancelを実行すればポーリングの中止も可能です。
強いて言うなら、cancelの処理を関数に切り出したとはいえ、setIntervalの中で呼んでるのが少し違和感があります。


## 3. EventEmitterを使ってみる

クロージャではなく、クラスで書いてみます。
EventEmitterはブラウザで動かしたい場合は、
```
$ npm i events
```
でインストールが必要になります。

```typescript
class Polling {
  eventEmitter: EventEmitter;
  intervalId?: NodeJS.Timer;

  constructor() {
    this.eventEmitter = new EventEmitter();
    this.eventEmitter.on("removeListener", () => {
      this.stop();
    });
  }

  private startInterval(callback: () => boolean, interval: number) {
    this.intervalId = setInterval(() => {
      console.log("intervalのcallback実行");
      const isDone = callback();
      if (isDone) {
        console.log(
          "callback実行結果がtrueになったのでcompleteイベントを発火します"
        );
        this.eventEmitter.emit("complete");
      }
    }, interval);
  }

  async start(callback: () => boolean, interval: number) {
    this.startInterval(callback, interval);
    return new Promise((resolve) => {
      this.eventEmitter.once("complete", () => {
        console.log("completeイベントを受け取りました");
        resolve("ok");
      });
    });
  }

  stop() {
    console.log("setIntervalの処理を停止します");
    clearInterval(this.intervalId);
  }
}
```

Pollingインスタンスは先ほどと同じく、startメソッド(Promiseを返す)とstopメソッドを持っているので、awaitすればポーリング終了を待つことができますし、stopを呼び出せば途中でポーリングをキャンセルすることも可能です。
クラスを使っていてもやってることは基本的に変わらないのですが、今回はEventEmitterを使ってみました。
EventEmitterを経由させることで、メソッド間の依存関係がなくなっています。(プライベートメソッドの呼び出しはある)
ブラウザで動かしたければeventsをinstallする必要があるのが玉に瑕です。


3パターン書いてみましたが、2,3ならどちらでもいいような気がしますね。
冒頭にも書いた通り、そもそもポーリングする機会があまりなかったり、汎用性があるわけではないので使い所がどれだけあるかと言われると微妙な感じがします。
もしよければ参考にしてやってください！

以上です。