【Deno】ReadableStreamでFizz Buzzして無限Fizz Buzzサーバーを建てる

## ReadableStream（Webストリーム）でFizz Buzzする

[ReadableStream](https://developer.mozilla.org/ja/docs/Web/API/ReadableStream)は細切れのデータを送受信できるAPIです。
今回はこのAPIを使ってFizz Buzzしていきたいと思います。

また、Denoを使うとストリーミングするサーバーを簡単に書くことができます。今回はDenoを使って無限FizzBuzzストリーミングサーバーを書いてみます。

（何を言っているかわからない方は https://dash.deno.com/playground/fizzbuzz にアクセスして実例を見てください。）

### 実装方法

WebストリームAPIには以下の3種類が存在します。

- ReadableStream - 細切れのデータを読み込み
- TransformStream - 細切れのデータを変換
- WritableStream - 細切れのデータを書き込み

これらは、以下のコードのようにメソッドチェーンで組み合わせて使います。


```ts
new ReadableStream()
  .pipeThrough(new TransformStream())
  // ...（中略） 
  .pipeThrough(new TransformStream())
  .pipeTo(new WritableStream())
```

このようにメソッドチェーンできる理由は、`.pipeThrough()`の返り値がReadableStreamになっているからです。
上のコードが細切れのデータを読み書きする様子を図で表すと、下の画像のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678625/6bd8af21-9a56-fe5c-13f4-76da7074a999.png)

図に書いた通り、Webストリームはpullストリームであり、ジェネレーターのように**遅延評価**されます。
WritableStreamの書き込み準備が整うとバケツリレーのように遡ってReadableStreamにデータが要求され、次にReadableStreamがデータを書き込むとまたバケツリレーされてWritableStreamへデータが届きます。

このことを利用すると、無限のデータを生成するReadableStreamを作ることができます。

#### 無限ストリームの作り方

上記のように、「データを要求された時にデータを書き込む」というやり方で実装します。これをするにはReadableStreamの作成時に`pull`メソッドを定義します。

```ts
new ReadableStream({
  pull(controller) { // データが要求された時の処理
    // ここにデータを送信する処理を書く
  }
})
```

`pull`メソッドには`controller`という引数が渡されます。この`controller`を使ってデータを書き込みます。


```ts
new ReadableStream({
  pull(controller) { // データが要求された時に
    controller.enqueue(data); // データを書き込み
  }
})
```

今回はFizzBuzzを送信したいので、FizzBuzzを`controller.enqueue`します。

```ts
let i = 0;
const readable = new ReadableStream<string>({
  pull(controller) {
    const val = i % 15 === 0
      ? "FizzBuzz"
      : i % 3 === 0
      ? "Fizz"
      : i % 5 === 0
      ? "Buzz"
      : i;
    controller.enqueue(`${val}`);
    i++;
  },
});
```

これで「無限にFizzBuzzが流れてくるストリーム」の完成です。

試しにストリームを読んでみましょう。実行にはDeno1.22を使います。
ストリームを読むには`for-await-of`文を使うことができます。

```ts
for await (const data of readable) {
  console.log(data);
}
```

これを`deno run <ファイル名>`コマンドで動かすと、無限にFizzBuzzが表示されます。

:::note warn

`for-await-of`文を使ったやり方は、DenoとNode.jsのみで使えます。[ブラウザでは絶賛実装中](https://github.com/whatwg/streams/issues/778#issuecomment-461341033)です。
ブラウザの場合は`.getReader()`を使います。

```ts
const reader = readable.getReader();
while (true) {
  console.log(await reader.read());
}
```

:::

なお無限に表示されるので、気が済んだらctrl-cで止めましょう。

## 無限Fizz Buzzサーバー

[Deno](https://deno.land/)はNode.jsの後継の新しいサーバーサイドJavaScriptランタイムです。

Denoを使うと、**サーバーとブラウザの間でReadableStreamをやり取りする**ことができます。（DenoとブラウザはどちらもWeb標準APIを使えるため）

### 実装方法

まずは無限にFizz Buzzが流れてくるサーバーを作ります。
`http://localhost:8000/fizzbuzz`にアクセスしたときにfizzbuzzをストリーミングするサーバーを書いていきます。

```ts:serve.ts
import {
  serve,
  Status,
  STATUS_TEXT,
} from "https://deno.land/std@0.142.0/http/mod.ts";

serve((request) => {
  const { pathname } = new URL(request.url);
  if (pathname === "/fizzbuzz") { // "/fizzbuzz"にアクセスしたら
    // fizzbuzzストリームを作成
    let i = 0;
    const readable = new ReadableStream<string>({
      pull(controller) {
        const val = i % 15 === 0
          ? "FizzBuzz"
          : i % 3 === 0
          ? "Fizz"
          : i % 5 === 0
          ? "Buzz"
          : i;
        controller.enqueue(`${val}`);
        i++;
      },
    });
    // 文字列のストリームをUint8Arrayのストリームに変換
    const uint8ArrayStream = readable.pipeThrough(new TextEncoderStream());
    // Response作成時に1番目の引数にストリームを渡す
    return new Response(uint8ArrayStream);
  }

  // それ以外の場合は404 Not Foundを表示
  return new Response(STATUS_TEXT.get(Status.NotFound), {
    status: Status.NotFound,
  });
});
```

基本的には`if (pathname === "/fizzbuzz") { return new Reaponse(fizzbuzzストリーム) }`という処理の流れになります。

一つ注意点として、**レスポンスに渡す前に文字列をUint8Arrayに変換する**というポイントがあります。
文字列のストリームをUint8Arrayのストリームに変換するには組み込みで用意されている変換ストリームである`TextEncoderStream`を利用します。
`TextEncoderStream`はTransformStreamの一種なので、`readable.pipeThrough(new TextEncoderStream())`と書くことでパイプすることができます。

この状態でサーバーを起動し、`http://localhost:8000/fizzbuzz`にアクセスすると、fizzbuzzのデータが（無限に）どんどん流れてくるのがわかると思います。

```shell
deno run --allow-net ./serve.ts
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678625/c4b5376b-dc2b-f64c-66d5-11a7f2fc7e2d.png)


## クライアント側でReadableStreamを受け取る

サーバーで送信したReadableStreamを、ブラウザで受け取る処理を書いていきます。

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script type="module">
      // body はUint8Arrayのストリーム
      const { body } = await fetch("/fizzbuzz");
      // Uint8Arrayのストリームを文字列のストリームに変換
      const fizzbuzzStream = body.pipeThrough(new TextDecoderStream());
      // 読み出してconsole.logで表示　
      const reader = fizzbuzzStream.getReader();
      while (true) {
        console.log(await reader.read());
      }
    </script>
  </head>
  <body></body>
</html>
```

ブラウザでもReadableStreamの基本的な操作は一緒です。

`fetch()`で`/fizzbuzz`というURLにリクエストすると、Responseオブジェクトが返ってきます。このResponseオブジェクトには先ほどサーバーで作成したResponseオブジェクトと同じ情報が入っています。
その中の`body`というプロパティを使うと、ReadableStreamでデータを読み出すことができます。（`const { body } = await fetch("/fizzbuzz");`の行。）

サーバー側では「文字列のストリームをUint8Arrayのストリームに変換する」という処理を書きましたが、ブラウザ側では「Uint8Arrayを文字列のストリームのストリームに変換する」という処理を書きます。これには`TextDecoderStream`を使います。（`TextEncoderStream`の逆ですね。）

これらの処理が書かれた`index.html`をサーバーから配信すれば、FizzBuzzストリームをブラウザで受け取ることができます。

```ts
import {
  serve,
  Status,
  STATUS_TEXT,
} from "https://deno.land/std@0.142.0/http/mod.ts";
import { contentType } from "https://deno.land/std@0.142.0/media_types/mod.ts";
import { delay } from "https://deno.land/std@0.142.0/async/mod.ts";

serve(async (request) => {
  const { pathname } = new URL(request.url);

  if (pathname === "/") { // "/"というパスにアクセスされた場合は
    // index.htmlを読み込み
    const { body } = await fetch(new URL("./index.html", import.meta.url));
    // index.htmlのデータをレスポンスで返す
    return new Response(body, {
      headers: { "Content-Type": contentType("html")! },
    });
  }

  if (pathname === "/fizzbuzz") {
    let i = 0;
    const readable = new ReadableStream<string>({
      async pull(controller) {
        // 送信スピードが速すぎてブラウザコンソールが埋まってしまうので、1秒間待機してから送信する
        await delay(1000);

        const val = i % 15 === 0
          ? "FizzBuzz"
          : i % 3 === 0
          ? "Fizz"
          : i % 5 === 0
          ? "Buzz"
          : i;
        controller.enqueue(`${val}`);
        i++;
      },
    });
    return new Response(readable.pipeThrough(new TextEncoderStream()));
  }

  return new Response(STATUS_TEXT.get(Status.NotFound), {
    status: Status.NotFound,
  });
});
```

ここまで実装すると、


> FizzBuzzストリーム
↓
TextEncoderStream（文字列→Uint8Array変換）
↓
（サーバーからブラウザへ送信）
↓
TextDecoderStream（Uint8Array→文字列変換）
↓
while文で読み出し

という処理の流れが完成します。

このサーバーを起動した状態で http://localhost:8000/ にアクセスすれば、ブラウザコンソールにfizzbuzzが流れてきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678625/62861c67-bebe-96ad-eebe-4508b1c964dd.png)

無事FizzBuzzがブラウザ側で読めていることが確認できました。

※なお、送信スピードが速すぎてブラウザコンソールが埋まってしまうので、サーバー側からデータを送信する時に、delay関数を使って1秒待機するようにしています。

## まとめ

- ReadableStreamは遅延評価なので無限にデータを流すことができる
- これを利用して無限FizzBuzzストリームを作ることができる
- Denoを使うとサーバー側で作成したReadableStreamをブラウザ側に渡すことができる

以上、無限Fizz Buzzストリームを配信するサーバーの作り方でした。

外部ライブラリを一切使わずにここまでできるので、Denoはすごいぞという感じです。

なお本記事で作成したサーバーは[deno deploy](https://deno.com/deploy)というサービスを使って公開しています。急にFizzBuzzストリームが必要になったときはhttps://fizzbuzz.deno.dev にアクセスしてお使いください。

https://dash.deno.com/playground/fizzbuzz