JavaScript/WritableStream

WritableStreamオブジェクト

JavaScriptにおけるWritableStreamは、ストリームベースのデータ処理を可能にする重要なコンポーネントです。ストリーミングAPIの一部として、データの書き込み先として機能します。このオブジェクトを利用することで、大量のデータを効率的に処理できるようになります。

基本概念

WritableStreamは、データを一度に全て処理するのではなく、チャンク(小さな断片)単位で処理することができます。これにより、メモリ使用量を抑えながら大きなデータセットを扱うことが可能です。

WritableStreamの最も基本的な使い方を見てみましょう:

const writableStream = new WritableStream({
  start(controller) {
    console.log('ストリームの開始');
  },
  write(chunk, controller) {
    console.log('データの書き込み:', chunk);
  },
  close() {
    console.log('ストリームの終了');
  },
  abort(reason) {
    console.error('ストリームが中断されました:', reason);
  }
});

WritableStreamのメソッド

WritableStreamオブジェクトには以下の主要なメソッドがあります:

// 新しいライターを取得
const writer = writableStream.getWriter();

// データを書き込む
writer.write('こんにちは、ストリーム!').then(() => {
  console.log('データが書き込まれました');
});

// 書き込みを終了する
writer.close().then(() => {
  console.log('ストリームが正常に閉じられました');
});

// 書き込みを中断する
writer.abort('エラーが発生しました').then(() => {
  console.log('ストリームが中断されました');
});

WritableStreamDefaultWriterの使用

WritableStreamを操作するには、通常WritableStreamDefaultWriterオブジェクトを使用します:

async function writeToStream() {
  const writer = writableStream.getWriter();
  
  try {
    await writer.write('最初のチャンク');
    await writer.write('2番目のチャンク');
    await writer.write('最後のチャンク');
    await writer.close();
  } catch (error) {
    console.error('書き込み中にエラーが発生しました:', error);
  } finally {
    writer.releaseLock();
  }
}

writeToStream();

WritableStreamの状態

WritableStreamは常に以下のいずれかの状態にあります:

状態 説明
writable ストリームが書き込み可能な状態
closing ストリームが閉じている途中の状態
closed ストリームが閉じられた状態
errored エラーが発生した状態

状態の確認方法:

const writer = writableStream.getWriter();

// 各状態を確認するためのプロパティ
console.log('準備完了?', !writer.closed);
console.log('閉じている?', writer.closed);
console.log('書き込み可能?', !writer.closed && !writer.desiredSize === null);

// 非同期で状態の変化を待つ
writer.closed.then(() => {
  console.log('ストリームが閉じられました');
});

実践的な例:ファイルをダウンロードして保存する

以下の例では、fetch APIとWritableStreamを組み合わせて、ファイルをダウンロードし保存します:

async function downloadAndSave(url, filename) {
  // ファイルのダウンロード
  const response = await fetch(url);
  const reader = response.body.getReader();
  
  // ファイルシステムアクセスAPIを使用
  const fileHandle = await window.showSaveFilePicker({
    suggestedName: filename,
    types: [{
      description: 'テキストファイル',
      accept: {'text/plain': ['.txt']}
    }]
  });
  
  // 書き込み用のストリームを作成
  const writable = await fileHandle.createWritable();
  
  // データをストリーミング
  while (true) {
    const {done, value} = await reader.read();
    
    if (done) break;
    
    await writable.write(value);
  }
  
  // 書き込みを完了
  await writable.close();
  console.log('ファイルが保存されました');
}

// 使用例
downloadAndSave('https://example.com/large-text-file.txt', 'downloaded-file.txt');

バックプレッシャーの処理

WritableStreamの重要な機能の一つに「バックプレッシャー」があります。これは、書き込み先が処理しきれないほど速くデータが送られてくる場合に、自動的にデータフローを調整する仕組みです:

async function writeWithBackpressure(writableStream, data) {
  const writer = writableStream.getWriter();
  
  for (const chunk of data) {
    // write()はバックプレッシャーを自動的に処理するPromiseを返す
    await writer.write(chunk);
    console.log(`チャンク "${chunk}" が書き込まれました`);
  }
  
  await writer.close();
  console.log('すべてのデータが書き込まれました');
}

// 大量のデータを生成
const largeDataset = Array.from({length: 1000}, (_, i) => `データ項目 ${i}`);

// カスタムストリームを作成(処理に時間がかかる操作をシミュレート)
const slowWritableStream = new WritableStream({
  write(chunk, controller) {
    return new Promise(resolve => {
      // 各チャンクの処理に100ミリ秒かかると仮定
      setTimeout(() => {
        console.log(`処理: ${chunk}`);
        resolve();
      }, 100);
    });
  }
});

// バックプレッシャーを使ってデータを書き込む
writeWithBackpressure(slowWritableStream, largeDataset);

TransformStreamとの連携

WritableStreamTransformStreamReadableStreamと組み合わせて使用することで、強力なデータ処理パイプラインを構築できます:

// テキストを大文字に変換するTransformStream
const uppercaseTransform = new TransformStream({
  transform(chunk, controller) {
    // 文字列を大文字に変換
    const uppercased = chunk.toUpperCase();
    controller.enqueue(uppercased);
  }
});

// 結果を表示するWritableStream
const consoleWritable = new WritableStream({
  write(chunk) {
    console.log('変換結果:', chunk);
  }
});

// パイプラインを作成
fetch('https://example.com/data.txt')
  .then(response => {
    // 入力ストリームを変換ストリームにパイプし、さらに出力ストリームにパイプする
    const readableStream = response.body;
    
    // ReadableStream → TransformStream → WritableStream
    return readableStream
      .pipeThrough(uppercaseTransform)
      .pipeTo(consoleWritable);
  })
  .then(() => console.log('処理が完了しました'))
  .catch(error => console.error('エラーが発生しました:', error));

ブラウザとNode.jsの違い

WritableStreamはブラウザとNode.jsの両方で利用できますが、使用方法に若干の違いがあります:

// ブラウザ環境
// Web Streams APIはグローバルに利用可能
const browserWritableStream = new WritableStream({...});

// Node.js環境(バージョン16以降)
// 'stream/web'モジュールからインポートする必要がある
const { WritableStream } = require('stream/web');
const nodeWritableStream = new WritableStream({...});

// または、ESモジュールを使用する場合
import { WritableStream } from 'stream/web';
const nodeWritableStream = new WritableStream({...});

まとめ

WritableStreamはJavaScriptのストリーミングAPIにおける重要なコンポーネントで、大量のデータを効率的に書き込むための手段を提供します。バックプレッシャーのサポートや他のストリームとの統合により、メモリ効率の良いデータ処理パイプラインの構築が可能になります。Web開発における大規模データ処理、ファイル操作、ネットワーク通信など、多くの場面で活用できる強力なツールです。

カテゴリ:JavaScript
カテゴリ:JavaScript