◆結論

Compression Streams API – Web API | MDN (mozilla.org)

例えばdeflateの解凍だけを関数にしたら以下のとおり。

export const inflate = (data: ReadableStream): Body => {
  const decompressedStream = data.pipeThrough(new DecompressionStream('deflate'));
  return new Response(decompressedStream);
};

export const inflateBlob = (data: Blob): Body => {
  const decompressedStream = data.stream().pipeThrough(new DecompressionStream('deflate'));
  return new Response(decompressedStream);
};

export const inflateArrayBuffer = (data: ArrayBuffer): Body => {
  const resp = new Response(data);
  if (!resp.body) return resp;
  const decompressedStream = resp.body.pipeThrough(new DecompressionStream('deflate'));
  return new Response(decompressedStream);
};

‘deflate’を’gzip’にすればgzipだし、DecompressionStreamをCompressionStreamにすれば圧縮できるし。

上記はTypeScriptなんで、JavaScriptで使おうと思ったら「:」の型指定を消して差し上げろ。exportも不要なら消せ。

◆いきさつ

Deflate圧縮されたファイルをInflateしたいねと思った。んで「JavaScript Inflate」とか「JavaScript Deflate」みたいに検索したところ、ライブラリを使う例しか出てこない。

あとはNode.jsのZlib

でもブラウザってAccept-Encodingヘッダーでgzipとか受け付けがちじゃん。gzを解凍できるんならDeflateもでしょう?と思って「Stream」とか検索ワードに入れつつ1分調べたらCompression Streams APIを見つけた。SafariとFirefoxでも2023年3~5月あたりから使えるようになってる。

WebのAPIだけど、Node.jsでもv17から取り込まれてたみたい。

Denoでも使えるBunも一応ググったけど良くわかんなかった。っていうかBunは不誠実なベンチマークで優位を主張したから使いたくない。倫理観のないプラットフォームに関わりたくない。

◆関数化とFetchのBodyのはなし

生でDecompressionStreamを使って何の問題も無いんだけど、よく使うテクニックとして結果のStreamをResponseのコンストラクタにぶっこんじゃうというのがある。一番初めの例で見せたみたいな。

ResponseはFetch APIのResponseそのものであるからして、バイト列をなんだかんだblob()とかtext()メソッドで取り出すのが便利。役割的にあんまり良くない気もするんだけど便利。

ただしtext()はUTF-8でしか取れないんで注意。

TypeScriptでいえばResponseはBodyというinterfaceを実装しているので、戻り値としてはBodyを返すことにしておけばいい。ResponseのみならずRequestもBodyを実装していたりする。

TypeScript/src/lib/dom.generated.d.ts at main · microsoft/TypeScript · GitHub

interface Body {
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) */
    readonly body: ReadableStream<Uint8Array> | null;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) */
    readonly bodyUsed: boolean;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/arrayBuffer) */
    arrayBuffer(): Promise<ArrayBuffer>;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/blob) */
    blob(): Promise<Blob>;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/formData) */
    formData(): Promise<FormData>;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
    json(): Promise<any>;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/text) */
    text(): Promise<string>;
}

見る限りbodyUsedを持ってるし、メモリコピーはしてないんじゃない?と勝手に思ってる。でもコピーしててもおかしくないし、パフォーマンスがクリティカルな場合はResponseインスタンスを経由せず素直にStreamのまま操作したほうがよいでしょうね。

◆気を付ける点

Deflate圧縮されていないバイナリを解凍しようとしたとき、失敗してくれない。例外とか起こしてくれない。チェック機構もなし。

あと、Streamは解放するまで一度しか使えない。例えば以下のコードは確定でこける。

const drawImage = async () => {
  const url = `https://example.com/a.bin`
  const resp = await fetch(url);
  if (!resp.ok || !resp.body) throw new Error(``);

  let bitmap = await createImageBitmap(await resp.blob());
  const inflated = await inflate(resp.body);
  bitmap = await createImageBitmap(await inflated.blob());
  canvas.value?.getContext("2d")?.drawImage(bitmap, (canvas.value?.width - bitmap.width) / 2, 0);
};

なぜなら、「await resp.blob())」でレスポンスを読んだ後に「resp.body」でstreamを読もうとしているから。以下のようにエラーが発生します。

Uncaught (in promise) TypeError: Failed to execute ‘pipeThrough’ on ‘ReadableStream’: Cannot pipe a locked stream

結果を2度使いたいときはResponse.blob()でBlobに固めて退避したり、Response.clone()とかしたりすればいい。

◆おまけ:パイプをつなぐ

バイナリを文字列にするのにTextDecoderって使うじゃん?それのStream版もあったりする。

こいつにパイプをつないでみる。つまり、InflateしてからShift_JISとして読むとこうなる。

export const hoge = (data: ReadableStream): ReadableStream<string> => {
  return data
    .pipeThrough(new DecompressionStream('deflate'))
    .pipeThrough(new TextDecoderStream('shift-jis'));
};

参考:TextDecoder: encoding property – Web APIs | MDN (mozilla.org)

これはメモリに優しくて便利っちゃ便利。だが、このReadableStream<string>の読み出しがなかなか扱いややこしいので基本的には手を出さないほうがいいかも。

read()という非同期メソッドで内容を読めるんだけど、細切れにしか文字列が取れてこない。全量を読み切るreadAll的なメソッドは用意されていない。そして、readを簡単に使えるようにするメソッドが(2023年6月現在)ブラウザに実装されてないんすね。

欲しす。テラ欲しす。

Can I Use見る限り、Firefoxでは使えてるみたい。

valuesってのもあんのね。

こいつらが揃えばコードがまぁまぁ簡潔に書けちゃうじゃん。とはいえ実装状況が悪いから悲しいのじゃん。

◆結

ES2023って何が乗るんだっけと思って見に行ったんだけど、やっぱあんまり機能増えない。もうちょい盛れたんちゃう?

proposals/finished-proposals.md at main · tc39/proposals · GitHub

あとMDNがまたBlog書き始めてたね。

MDN Blog (mozilla.org)

わりかしちゃんと興味深いこと書かれてるんでWeb開発者はチェケラよ。最新の記事を観に行ったらCompression Streams APIにも触れられてるし。Web GPUにもWebTransportにも触れられてるし。全てあちあちAPIなので必修です。WebGLもWebSocketも消せるところは消していこう。

ドキュメントの話題だけどBaselineのはなしも見て損はない。どっかで保守しきれなくなって滅びそうな概念ではあるが。