Streamを撲滅するAsyncIteratorをNode.js v10未満で使う

StreamとPromiseの世界をひとつにする革命的な機能、**AsyncIterator**を知っていますか？

自分はこちらのスライドで知りました。
https://speakerdeck.com/ajido/callback-to-promise-and-beyond

[![Callbackを撲滅した後に _ Callback to Promise and beyond - Speaker Deck](https://qiita-image-store.s3.amazonaws.com/0/70109/434bcd9a-bb0e-c816-5296-1cd5c2712a21.jpeg)](https://speakerdeck.com/ajido/callback-to-promise-and-beyond?slide=17)

[![Callbackを撲滅した後に _ Callback to Promise and beyond - Speaker Deck](https://qiita-image-store.s3.amazonaws.com/0/70109/4bc1cb5d-1635-ed84-2c58-c5e30e55304a.jpeg)](https://speakerdeck.com/ajido/callback-to-promise-and-beyond?slide=20)

AsyncIteratorは便利なのですが、残念なことにNode.js v10で導入された機能で、それ以上でしか使えません。

しかし、Google Cloud Functionsなど多くのPaaS / FaaSではまだNode.js v6やv8のランタイムしか使用できず、そのままではAsyncIteratorが使用できませんでした。

そこで、Node.js v10未満でもAsyncIteratorを使用できるようにする方法を考えます。

登場する単語などはJavaScriptのIterator / Generatorに関する記事を参照してください。
オススメは以下です。

- [JavaScript の イテレータ を極める！](https://qiita.com/kura07/items/cf168a7ea20e8c2554c6)
- [JavaScript の ジェネレータ を極める！](https://qiita.com/kura07/items/d1a57ea64ef5c3de8528)

# 前提

- Node.js v6以上（ `readable-stream` v3の対応に合わせて）
- 実行はDockerの `node:6` と `node:8` 上で行ってテスト
- BabelもしくはTypeScriptなどを利用して、下位バージョンでも動作する構文にトランスパイルする（使用するラインタイムに合わせて適当なターゲットを設定）
- 設定ファイルの参考は以下

```json:.babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": "6"
        }
      }
    ]
  ]
}
```

```json:tsconfig.json
{
  "compilerOptions": {
    "lib": [
      "dom",
      "es6",
      "dom.iterable",
      "scripthost",
      "esnext.asynciterable"
    ],
    "module": "commonjs",
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "outDir": "lib",
    "sourceMap": true,
    "strict": true,
    "target": "es2015"
  },
  "compileOnSave": true,
  "include": [
    "src"
  ]
}
```

### ゴール

`axios` を使って画像をダウンロードし、AsyncIterator（ `for await...of` ）とAsync / Await で制御しつつ、Node.js v6以上で動作させる。


# Polyfillを利用する

最初に思いつく対応策は**Polyfill**です。

Polyfillのパッケージとしては [`@babel/polyfill`](https://www.npmjs.com/package/@babel/polyfill) が有名です。
これは内部的には [`core-js`](https://github.com/zloirock/core-js/tree/v2) に依存しており、実質同じものですのでどちらを利用しても構いません。

実際にPolyfillを利用することで、自分で実装したAsyncIteratorには対応することができます。

```typescript
import "core-js/modules/es7.symbol.async-iterator";
// or
// import "@babel/polyfill";

function* IterableIterator() {
  yield 1;
  yield 2;
  yield 3;
}

(async () => {
  for await (const chunk of IterableIterator()) {
    console.log(chunk);
  }
})();
```

Polyfillを読み込まなかった場合は以下のようなエラーが発生しますが、読み込ませると発生しません。

```bash:Polyfillなし
$ node lib/index.js
(node:12) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): TypeError: Symbol.asyncIterator is not defined.
```

```bash:Polyfillあり
$ node lib/index.js
1
2
3
```

# `readable-stream` を使用して標準Streamを置き換える

Polyfillを読み込ませることでGeneratorで作成した独自のIterableなIteratorを `for await...of` で制御することはできるようになりました。

しかし多くの場合、AsyncIteratorを使用したいのは**Streamでの非同期処理をシンプルにしたい**という動機かと思います。

AsyncIteratorを使用するためには、オブジェクトが `[Symbol.asyncIterator]` メソッドを持っている必要があります。
Node.jsのコアAPIである[Stream](https://nodejs.org/api/stream.html)は、Node.js v10未満では `[Symbol.asyncIterator]` メソッドを持っていません。これが最大の問題です。

![readable[Symbol.asyncIterator]()](https://qiita-image-store.s3.amazonaws.com/0/70109/07323d47-112e-d120-450c-63029c168cc1.png)

**この問題に対処するために [`readable-stream`](https://github.com/nodejs/readable-stream) パッケージを使用します。**

これは、Node.jsオフィシャルのGitHubで管理されているパッケージで、Node.jsのStreamをNode.jsのバージョンとは切り離して使用できるようにするものです。

これを普段 `import * as stream from "stream";` とする代わりに `import * as stream from "readable-stream";` と記述して使用します。

```typescript
import "core-js/modules/es7.symbol.async-iterator";
// or
// import "@babel/polyfill";

// import * as stream from "stream";
import * as stream from "readable-stream";

const IterableIterator = new stream.Readable({
  read() {
    this.push("hello");
    this.push(null);
  }
});

(async () => {
  for await (const chunk of IterableIterator) {
    console.log(chunk.toString());
  }
})();
```

一見、この `readable-stream` だけあればPolyfillは必要ないように思えるかもしれませんが、そもそもNode.js v9未満では `[Symbol.asyncIterator]` などのメソッドに対応していないため、これを `@@asyncIterator` に変換しなければいけないそうです。

**\* すみません、ここは理解が曖昧です**

実際Polyfillなしで実行すると以下のようなエラーが発生します。

```bash:Polyfillなし
$ node lib/index.js
(node:42) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): TypeError: Symbol.asyncIterator is not defined.
```

まとめると以下の表のようになります。
どちらも必要ということですね。

~ | `readable-stream`: _enable_ | `readable-stream`: _disable_
:--: | :--: | :--:
**`core-js`: _enable_ / Node.js: _>=10_** | 👍 | 👎
**`core-js`: _disable_** | 👎 | 👎

# 外部パッケージで `readable-stream` を使う

さて、ゴールとして実行したいのは `axios` パッケージを使用してダウンロードすることでした。
先ほどのように自分でStreamを作成する場合は `readable-stream` のAPIを使用すればAsyncIteratorを使用することができますが、外部パッケージの場合はどうしたらいいでしょうか。

最初はパッケージをフォークして、ソースコードを修正しなければいけないかと思ったのですが、もっと簡単な方法がありました。

AsyncIteratorを使用したいStreamに `[Symbol.asyncIterator]` があればいいので、 `readable-stream` から読み込んだ `stream.Readable.prototype[Symbol.asyncIterator]` を代入してあげれば `readable-stream` を使用してStreamを作成した場合と同様になります。

複雑なようですがコードは単純です。

```typescript
yourReadableStream[Symbol.asyncIterator] = stream.Readable.prototype[Symbol.asyncIterator]
```

ゴールのコードの全体は以下のようになります。

```typescript
import "core-js/modules/es7.symbol.async-iterator";
// or
// import "@babel/polyfill";

import * as fs from "fs";
import * as stream from "readable-stream";
import axios from "axios";
import * as mime from "mime";

const download = async (url: string) => {
  const response = await axios({
    method: "get",
    url,
    responseType: "stream"
  });

  response.data[Symbol.asyncIterator] =
    stream.Readable.prototype[Symbol.asyncIterator];

  const file = `file.${mime.getExtension(response.headers["content-type"])}`;
  const ws = fs.createWriteStream(file);

  for await (const data of response.data) {
    ws.write(data);
  }
};

(async () => {
  await download("https://avatars1.githubusercontent.com/u/3258736?v=4");
})();
```

# まとめ

以下の手順で処理することで、Node.js v6 - v9でもAsyncIteratorを実行することができるようになりました。

1. `readable-stream` でStream APIに `[Symbol.asyncIterator]` メソッドを追加する
2. `core-js` で `[Symbol.asyncIterator]` を `@@asyncIterator` に変換する
3. BabelやTypeScriptなどのトランスパイラで `for await...of` などの未対応の糖衣構文を下位バージョンでも実行できる構文に変換する

個人的には、今後のStreamはすべてAsyncIteratorで記述したいと思っているので、様々な環境で実行できるようになって満足です。

**謝辞**

GitHub Issuesにて質問に答えていただいたNode.jsコミッターの[mcollina](https://github.com/mcollina)さん、ありがとうございました！
https://github.com/nodejs/readable-stream/issues/400