【Deno1.19】Denoのストリーム3種と入出力インターフェース

Denoには主に2種類のストリームが実装されている。Go言語をモデルにした入出力インターフェースと、Web標準のストリームである。
これらに加えてイテレータもストリームのように扱える。これら3つは相互に変換可能である。

大事なのは、**これらはNode.jsのストリームとは全く異なるもの**だということ。混乱の元なので気を付けたい。

:::note warn
この記事はDeno1.19のリリースを受けて大幅に改稿し、
改稿時には
・Deno.FileからDeno.FsFileへの改名
・stdin/stderr/stdoutやDeno.FsFileのWebストリーム対応
・std/ioからstd/streamsへの再編
についての情報を反映した。
:::

この記事ではまず**Webストリーム**について解説し、次に**Go由来のストリーム**について解説し、最後に2つの相互変換について解説する。

## Webストリーム

Webストリームはブラウザ互換のストリーム。

- `ReadableStream`（[MDN](https://developer.mozilla.org/ja/docs/Web/API/ReadableStream)）
- `WritableStream`（[MDN](https://developer.mozilla.org/ja/docs/Web/API/WritableStream)）
- `TransformStream`（[MDN](https://developer.mozilla.org/ja/docs/Web/API/TransformStream)）

の3種のインターフェースからなる。現在Denoに実装されているAPIのうち、

- `Deno.FsFile`：ファイルシステム（[doc](https://doc.deno.land/deno/stable/~/Deno.FsFile)）
- `Deno.stdin.readable`：標準入出力（[doc](https://doc.deno.land/deno/stable/~/Deno.stdin)）
- `Deno.stdout.writable`（[doc](https://doc.deno.land/deno/stable/~/Deno.stdout)）
- `Deno.stderr.writable`（[doc](https://doc.deno.land/deno/stable/~/Deno.stderr)）
- `Deno.Process.stdin.writable`：サブプロセス入出力（[doc](https://doc.deno.land/deno/stable/~/Deno.Process)）
- `Deno.Process.stdout.readable`（[doc](https://doc.deno.land/deno/stable/~/Deno.Process)）
- `Deno.Process.stderr.readable`（[doc](https://doc.deno.land/deno/stable/~/Deno.Process)）
- `TextDecoderStream` API：Uint8Arrayと文字列の変換（[MDN](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoderStream)）
- `TextEncoderStream` API（[MDN](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoderStream)）
- `CompressionStream` API：gzip圧縮の変換（[MDN](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream)）
- `DecompressionStream` API（[MDN](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream)）
- `Blob.stream()` API（[MDN](https://developer.mozilla.org/ja/docs/Web/API/Blob)）
- `fetch` API：body等に渡せる（[MDN](https://developer.mozilla.org/ja/docs/Web/API/fetch)）
  - `Request`（[MDN](https://developer.mozilla.org/ja/docs/Web/API/Request)）
  - `Response`（[MDN](https://developer.mozilla.org/ja/docs/Web/API/Response)）
- `WebSocketStream` API（[doc](https://doc.deno.land/deno/unstable/~/WebSocketStream)）

が対応済み。

`ReadableStream`から取得したデータを`TransformStream`で変換し、`WritableStream`へ流し込むことができる。

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678625/5b0a3109-eee2-9650-7566-2636bd51f5fd.png)
https://developer.mozilla.org/ja/docs/Web/API/Streams_API/Concepts#pipe_chains



#### Webストリームの操作方法

Webストリームの操作方法については、Web標準ということもあり他の記事でも開設されているため、詳しくは取り上げない。

##### ReadableStreamの読み取り

ReadableStreamはWritableStreamやTransformStreamへパイプするのが基本であるが、for-await文でも読み出すことができる。

```ts
for await (const chunk of Deno.stdin.readable) {
  console.log(chunk); //=> 細切れになったデータがUint8Arrayで流れてくる
}
```

##### ReadableStreamからWritableStreamへのパイプ

ReadableStreamから読み取ったデータをWritableStreamに流すには、[`ReadableStream.pipeTo()`](https://developer.mozilla.org/ja/docs/Web/API/ReadableStream/pipeTo)を使用する。

`ReadableStream`は、`Deno.stdin.readable`や`file.readable`のように取得する。
`WritableStream`は、`Deno.stdout.writable`や`file.writable`のように取得する。

```ts
// Deno.openの返り値はDeno.FsFile
const file = await Deno.open("./foo.txt");
file.readable.pipeTo(Deno.stdout.writable); // fileから読み取ったデータをstdoutにパイプ
```

```ts
// Deno.openの返り値はDeno.FsFile
const file = await Deno.open("./foo.txt");
Deno.stdin.readable.pipeTo(file.writable); // stdinから読み取ったデータをfileにパイプ
```


##### 1行ずつの読み込み

Webストリームの変換には`TransformStream`と`ReadableStream.pipeThrough()`関数を使う。
ここでは、

- Uint8Arrayのストリームを文字列のストリームに変換する[TextDecoderStream](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoderStream)（Web API）
- 文字列のストリームを1行ずつの形に変換する[TextLineStream](https://doc.deno.land/https://deno.land/std@0.136.0/streams/mod.ts/~/TextLineStream)（Deno標準ライブラリ）

の2種類のTransformStreamを使用する例を挙げる。

```ts
import { TextLineStream } from "https://deno.land/std@0.136.0/streams/mod.ts";

// Deno.openの返り値はDeno.FsFile
const file = await Deno.open("./foo.txt");

const lineStream = file.readable // fileからReadableStreamで読み出し
    .pipeThrough(new TextDecoderStream()) // Uint8Arrayをstringに変換
    .pipeThrough(new TextLineStream()) // 1行ずつに変換

for await (const line of lineStream) {
  console.log(line); //=> 1行ずつ表示
}
```

Deno標準ライブラリの[streamsモジュール](https://doc.deno.land/https://deno.land/std@0.136.0/streams/mod.ts)では、他にもいくつかの`TransformStream`が提供されており、同様に扱うことができる。

##### その他のストリーム処理

- ストリームのマージ（順序を考慮する）：[zipReadableStreams関数](https://doc.deno.land/https://deno.land/std@0.125.0/streams/mod.ts/~/zipReadableStreams)
- ストリームのマージ（順序を考慮しない）：[mergeReadableStreams関数](https://doc.deno.land/https://deno.land/std@0.125.0/streams/mod.ts/~/mergeReadableStreams)
- ストリームの分岐：[ReadableStream.teeメソッド](https://developer.mozilla.org/ja/docs/Web/API/ReadableStream/tee)
- 改行で分割するTransformStream：[TextLineStreamクラス](https://doc.deno.land/https://deno.land/std@0.136.0/streams/mod.ts/~/TextLineStream)
- 区切り文字で分割するTransformStream：[TextDelimiterStreamクラス](https://doc.deno.land/https://deno.land/std@0.136.0/streams/mod.ts/~/TextDelimiterStream)
- バイト列で分割するTransformStream：[DelimiterStreamクラス](https://doc.deno.land/https://deno.land/std@0.136.0/streams/mod.ts/~/DelimiterStream)
- 指定したバイト数の分だけ読み出すTransformStream：[LimitedTransformStreamクラス](https://doc.deno.land/https://deno.land/std@0.136.0/streams/mod.ts/~/LimitedTransformStream)
- 指定した数の分だけchunkを読み出すTransformStream：[LimitedBytesTransformStreamクラス](https://doc.deno.land/https://deno.land/std@0.136.0/streams/mod.ts/~/LimitedBytesTransformStream)
- CSV形式のテキストを配列のストリームに変換するTransformStream：[CSVStreamクラス](https://doc.deno.land/https://deno.land/std@0.136.0/encoding/csv/stream.ts/~/CSVStream)

例えばTextLineStreamとLimitedBytesTransformStreamを組み合わせると、「先頭からn行読み込み」のような事ができる。

## Go由来の入出力インターフェース

ここから先は、Webストリームが導入される前に使われていた、Go由来の入出力インターフェースについて解説する。
Go由来の入出力インターフェースは以下。

- `Deno.Reader`
- `Deno.ReaderSync`
- `Deno.Writer`
- `Deno.WriterSync`
- `Deno.Seeker`
- `Deno.SeekerSync`
- `Deno.Closer`

これらは、Denoの入出力APIである

- `Deno.FsFile`
- `Deno.conn`
- `Deno.stdin`
- `Deno.stdout`
- `Deno.stderr`
- `Deno.Process.stdin`
- `Deno.Process.stdout`
- `Deno.Process.stderr`

に実装されている。
（注：ここでは、インターフェースというのはTypeScriptのinterfaceのこと。）

それぞれ、どのクラスにどのインターフェースが実装されているものかを一覧にしたものが下表。

||`Deno.Reader`|`Deno.ReaderSync`|`Deno.Writer`|`Deno.WriterSync`|`Deno.Seeker`|`Deno.SeekerSync`|`Deno.Closer`|
-|-|-|-|-|-|-|-
|`Deno.FsFile`<br>※ファイル読み込み|○|○|○|○|○|○|○|
|`Deno.conn`<br>※Http接続|○||○||||○|
|`Deno.stdin`|○|○|||||○|
|`Deno.stdout`|||○|○|||○|
|`Deno.stderr`|||○|○|||○|
|`Deno.Process.stdin`<br>※サブプロセスのstdin|||○||||○|
|`Deno.Process.stdout`<br>※サブプロセスのstdout|○||||||○|
|`Deno.Process.stderr`<br>※サブプロセスのstderr|○||||||○|

これらのインターフェースと標準ライブラリを使うことで、「標準入力からファイルへ」「ファイルからサブプロセスへ」等、それぞれ相互にストリームをパイプすることができる。

#### Go由来の入出力インターフェースの操作方法

標準ライブラリの`stream`を用いて操作する。以下は一例。詳しくは公式ドキュメント（https://doc.deno.land/https://deno.land/std/streams/mod.ts ）を参照。

なお、これらの関数はかつては`Deno.xxx`に存在したが、標準ライブラリに移動されたという経緯がある（例えば`Deno.copy`は標準ライブラリの`io/copy`を経て`streams/copy`に移動した）。古い記事ではこれらの変更が反映されていないことがあるので注意。（[参考](https://github.com/denoland/deno/issues/9795)）

##### データの読み書き

基本となるデータの読み書きは[`readAll`関数](https://doc.deno.land/https://deno.land/std@0.125.0/streams/mod.ts/~/readAll)(読み取り)と[`writeAll`関数](https://doc.deno.land/https://deno.land/std@0.125.0/streams/mod.ts/~/writeAll)(書き込み)を使う。

```ts
import { readAll, writeAll } from "https://deno.land/std@0.125.0/streams/mod.ts";

{
  // Deno.openの返り値はReaderかつWriter
  const file = await Deno.open("my_file.txt", {read: true});
  const content = new TextDecoder.decode(await readAll(file));
  file.close();
  console.log(content);
}

{
  const content = "Hello World";
  // Deno.openの返り値はReaderかつWriter
  const file = await Deno.open("my_file.txt", {read: true});
  await writeAll(file, new TextEncoder().encode(content));
  file.close();
}
```

##### ReaderからWriterへのパイプ

[`copy`関数](https://doc.deno.land/https://deno.land/std/streams/mod.ts/~/copy)を使うとReaderからWriterへストリームをパイプすることができる。

```ts
import { copy } from "https://deno.land/std@0.125.0/streams/mod.ts";

// Deno.openの返り値であるDeno.FsFileはReaderかつWriter
const reader = await Deno.open("my_file.txt");
const writer = await Deno.open("my_file.txt");

// readerのデータをwriterへコピー
await copy(reader, writer);

reader.close();
writer.close();
```

##### 1行ずつの読みこみ

標準ライブラリのioモジュールから、[`readLines`関数](https://doc.deno.land/https://deno.land/std@0.125.0/io/mod.ts/~/readLines)を使う。（streamsモジュールではないので注意）

[ioモジュール](https://doc.deno.land/https://deno.land/std/io/mod.ts)には他にも、指定した区切り文字で分割する[`readDelim`関数](https://doc.deno.land/https://deno.land/std@0.125.0/io/mod.ts/~/readDelim)など、reader操作系の関数が揃っている。

```ts
import { readLines } from "https://deno.land/std@0.125.0/io/mod.ts";

// Deno.openの返り値であるDeno.FsFileはReaderかつWriter
const reader = await Deno.open("my_file.txt");

// 1行ずつ読み込み
for await (const line of readLines(reader)) {
  console.log(line)
}

reader.close();
```

##### その他

[encoding/binaryモジュール](https://doc.deno.land/https://deno.land/std@0.125.0/encoding/binary.ts)にもDeno.Readerを操作する関数がある。

## WebストリームとGo由来のストリームの相互変換

これまで紹介したストリーム2種と、[イテレータ（Iterable）](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Iteration_protocols)は相互に変換できる。

#### Webストリーム⇔Go由来ストリーム

- [writerFromStreamWriter](https://doc.deno.land/https://deno.land/std@0.125.0/streams/mod.ts/~/writerFromStreamWriter)（`WritableStream`→`Deno.writer`）
- [writableStreamFromWriter](https://doc.deno.land/https://deno.land/std@0.125.0/streams/mod.ts/~/writableStreamFromWriter)（`Deno.writer`→`WritableStream`）
- [readerFromStreamReader](https://doc.deno.land/https://deno.land/std@0.125.0/streams/mod.ts/~/readerFromStreamReader)（`ReadableStream`→`Deno.reader`）
- [readableStreamFromReader](https://doc.deno.land/https://deno.land/std@0.125.0/streams/mod.ts/~/readableStreamFromReader)（`Deno.reader`→`ReadableStream`）

```ts
import {
  writerFromStreamWriter,
  writableStreamFromWriter,
  readerFromStreamReader,
  readableStreamFromReader,
} from "https://deno.land/std@0.125.0/streams/mod.ts";

const writer = writerFromStreamWriter(writable);
const writable = writableStreamFromWriter(writer);
const reader = readerFromStreamReader(readable);
const readable = readableStreamFromReader(reader);
```

#### Go由来ストリーム⇔イテレータ

- [readerFromIterable](https://doc.deno.land/https://deno.land/std@0.125.0/streams/mod.ts/~/readerFromIterable)（イテレータ→`Deno.reader`）
- [iterateReader](https://doc.deno.land/https://deno.land/std@0.125.0/streams/mod.ts/~/iterateReader)（`Deno.reader`→イテレータ）

```ts
import { delay } from "https://deno.land/std@0.125.0/async/delay.ts";
import { iterateReader, readerFromIterable } from "https://deno.land/std@0.125.0/streams/mod.ts";

async function* asyncIterable() {
  while (true) {
    await delay(1000);
    yield new TextEncoder().encode("hey^^");
  }
}

// イテレータ→`Deno.reader`
const reader = readerFromIterable(asyncIterable());

// `Deno.reader`→イテレータ
for await (const chunk of iterateReader(reader)) {
  console.log(chunk);
}
```

#### Webストリーム⇔イテレータ


- [readableStreamFromIterable](https://doc.deno.land/https://deno.land/std@0.125.0/streams/mod.ts/~/readableStreamFromIterable)（イテレータ→`ReadableStream`）
- ReadableStream[Symbol.asyncIterator]

※ReadableStreamには`Symbol.asyncIterator`が生えているため、そのまま`for-await-of`文で回すことができる。

```ts
import { delay } from "https://deno.land/std@0.125.0/async/delay.ts";
import { readableStreamFromIterable } from "https://deno.land/std@0.125.0/streams/mod.ts";

async function* asyncIterable() {
  while (true) {
    await delay(1000);
    yield new TextEncoder().encode("hey^^");
  }
}

// イテレータ→`ReadableStream`
const readableStream = readableStreamFromIterable(asyncIterable());

// `ReadableStream`→イテレータ
// ReadableStreamはそのまま`for-await-of`文で回すことができる
for await (const chunk of readableStream) {
  console.log(chunk);
}
```

## Node.jsストリームのポリフィル

Deno標準ライブラリには、[Node.jsストリームのポリフィルが存在する。](https://doc.deno.land/https://deno.land/std@0.125.0/node/stream.ts)

主にNode.js互換モードやDeno対応npmレジストリで使われるものであり、自分の手で使用する機会は少ないと思われる。



## ストリームの代わりに使えるもの

特に標準入出力やファイル等は、ストリームを扱わなくても使える便利関数が用意されている。

- 標準入出力系
  - alert関数（[doc](https://doc.deno.land/deno/stable/~/alert)）
  - confirm関数（[doc](https://doc.deno.land/deno/stable/~/confirm)）
  - prompt関数（[doc](https://doc.deno.land/deno/stable/~/prompt)）
  - printf関数（[doc](https://doc.deno.land/https://deno.land/std@0.125.0/fmt/printf.ts)）
- ファイル読み書き系
  - Deno.readTextFile（[doc](https://doc.deno.land/deno/stable/~/Deno.readTextFile)）
  - Deno.readTextFileSync（[doc](https://doc.deno.land/deno/stable/~/Deno.readTextFileSync)）
  - Deno.readFile（[doc](https://doc.deno.land/deno/stable/~/Deno.readFile)）
  - Deno.readFileSync（[doc](https://doc.deno.land/deno/stable/~/Deno.readFileSync)）
  - Deno.writeTextFile（[doc](https://doc.deno.land/deno/stable/~/Deno.writeTextFile)）
  - Deno.writeTextFileSync（[doc](https://doc.deno.land/deno/stable/~/Deno.writeTextFileSync)）
  - Deno.writeFile（[doc](https://doc.deno.land/deno/stable/~/Deno.writeFile)）
  - Deno.writeFileSync（[doc](https://doc.deno.land/deno/stable/~/Deno.writeFileSync)）

ストリームを使う必要が無い場合は、Deno.readTextFile等、上記のAPIを使ったほうが簡単に書ける（こともある）。

## まとめ

- DenoにはGo言語由来の入出力インターフェースと、Web標準のストリームの、2種類のストリームがある
- これらは相互に変換可能である
- Node.jsのストリームとは全く別物

なお、従来DenoにはGo言語由来の入出力インターフェースのみ存在していたが、後々Web APIが拡充されるに従ってWebストリームに対応するAPIが増えており、将来的にはWebストリームに一本化される予定である。