# 帳票設計書 13-SSR WebWritable出力

## 概要

本ドキュメントは、Vue.jsのサーバーサイドレンダリング（SSR）パッケージ `@vue/server-renderer` における `pipeToWebWritable` 関数の出力仕様を定義する帳票設計書である。この関数は、Vueアプリケーションのレンダリング結果をWeb WritableStreamにパイプ出力する。CloudFlare Workers等のWeb Streams環境向けに設計されており、writer.readyの互換性対応を含む。

### 本帳票の処理概要

**業務上の目的・背景**：近年、CloudFlare Workers、Deno Deploy、Vercel Edge Functions等のEdge Runtime環境が普及しており、これらの環境ではNode.jsのStreamモジュールが利用できない。代わりにWeb Streams APIが標準として提供される。`pipeToWebWritable` はこうしたWeb Streams環境でVueのSSRストリーミングを可能にするAPIであり、既存のWritableStreamインスタンスにレンダリング結果をパイプする。また、CloudFlare Workersでwriter.readyプロパティが実装されていない問題（#4287）への互換性対応も含まれている。

**帳票の利用シーン**：(1) CloudFlare WorkersでVueアプリケーションのSSRを行う場合、(2) Deno等のWeb Streams対応ランタイムでSSRストリーミングを行う場合、(3) ブラウザ互換のWritableStreamにSSR結果を出力する場合、(4) Edge Runtime環境でのSSRストリーミングが求められる場合。

**主要な出力内容**：
1. VueアプリケーションまたはVNodeから生成されたHTML文字列がTextEncoderでエンコードされてWritableStreamのwriterに逐次書き込まれる
2. レンダリング完了時にwriter.close()が呼ばれてストリームが正常終了する
3. エラー発生時にはconsole.logでエラーが出力され、writer.close()が呼ばれる

**帳票の出力タイミング**：pipeToWebWritable関数が呼び出された直後に非同期でレンダリングが開始される。内部でrenderToSimpleStreamを呼び出し、SimpleReadableインターフェースのアダプターを経由してWritableStreamのwriterにHTMLチャンクがTextEncoder経由でエンコードされて逐次書き込まれる。

**帳票の利用者**：Edge Runtime環境（CloudFlare Workers、Deno Deploy、Vercel Edge Functions等）でSSRストリーミングを利用するサーバーサイド開発者、Web Streams API対応アプリケーション開発者。

## 帳票種別

ストリーミングHTML出力（Web WritableStreamへのパイプ出力）

## 利用画面

| 画面No | 画面名 | URL/ルーティング | 出力操作 |
|--------|--------|-----------------|---------|
| N/A | Edge Runtime/サーバーサイド処理 | N/A | pipeToWebWritable関数呼び出し |

## 出力形式

### 基本仕様

| 項目 | 内容 |
|-----|------|
| ファイル形式 | エンコード済みバイトストリーム（TextEncoder経由のUint8Array） |
| 用紙サイズ | N/A（プログラム出力） |
| 向き | N/A |
| ファイル名 | N/A（ストリーム出力） |
| 出力方法 | Web WritableStreamのwriter.write()/writer.close()メソッド経由 |
| 文字コード | UTF-8（TextEncoderによるエンコード） |

### PDF固有設定

N/A（本帳票はバイトストリーム出力であるため該当しない）

### Excel固有設定

N/A（本帳票はバイトストリーム出力であるため該当しない）

## 帳票レイアウト

### レイアウト概要

出力はVueコンポーネントツリーをSSRでレンダリングしたHTML文字列をTextEncoderでバイト列に変換し、WritableStreamのwriterに逐次書き込む構造である。

```
┌─────────────────────────────────────┐
│    pipeToWebWritable                │
│    ┌─────────────────────────────┐  │
│    │ writer = writable.getWriter │  │
│    │ encoder = new TextEncoder   │  │
│    │                             │  │
│    │ SimpleReadableアダプター      │  │
│    │  async push(content)        │  │
│    │    ├─ [hasReady] await      │  │
│    │    │   writer.ready         │  │
│    │    ├─ content != null       │  │
│    │    │   → writer.write(      │  │
│    │    │     encoder.encode())  │  │
│    │    └─ content == null       │  │
│    │        → writer.close()     │  │
│    │  destroy(err)               │  │
│    │    → console.log(err)       │  │
│    │    → writer.close()         │  │
│    └─────────────────────────────┘  │
│              │                      │
│              ▼                      │
│    renderToSimpleStream             │
│    （内部基盤API）                    │
└─────────────────────────────────────┘
```

### ヘッダー部

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 1 | エンコード済みHTMLチャンク | VNodeツリーから生成されたHTML文字列をTextEncoderでエンコードしたUint8Array | renderToSimpleStream → SimpleReadable.push → TextEncoder.encode | writer.write(Uint8Array) |

### 明細部

| No | 項目名 | 説明 | データ取得元 | 表示形式 | 列幅 |
|----|-------|------|-------------|---------|-----|
| 1 | writer.write(encodedChunk) | エンコード済みHTMLチャンクのwriterへの書き込み | push(content) where content != null | Uint8Array | N/A |
| 2 | writer.close() | ストリーム終了（正常完了またはエラー時） | push(null) or destroy(err) | N/A | N/A |

### フッター部

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 1 | Teleportコンテンツ | SSRContext.teleportsに格納されるTeleport解決結果 | resolveTeleports(context) | Record<string, string> |

## 出力条件

### 抽出条件

| 条件名 | 説明 | 必須 |
|-------|------|-----|
| input | Vueアプリケーションインスタンス（App）またはVNode | Yes |
| context | SSRContextオブジェクト（デフォルト: {}） | No（デフォルト値あり） |
| writable | Web WritableStreamインスタンス | Yes |

### ソート順

| 優先度 | 項目 | 昇順/降順 |
|-------|------|---------|
| 1 | VNodeツリーの深さ優先順序 | ツリー走査順 |

### 改ページ条件

N/A（ストリーム出力のため改ページの概念はない）

## データベース参照仕様

### 参照テーブル一覧

N/A（データベースアクセスは行わない）

### テーブル別参照項目詳細

N/A

## 計算仕様

### 計算項目一覧

| 項目名 | 計算式 | 端数処理 | 備考 |
|-------|-------|---------|------|
| TextEncoderエンコード | encoder.encode(content) でHTML文字列をUint8Arrayに変換 | N/A | 188行目でTextEncoderインスタンスを生成 |
| writer.ready判定 | isPromise(writer.ready) でreadyプロパティの有無を判定 | N/A | CloudFlare Workers互換性対応（#4287）、191-194行目 |

## 処理フロー

### 出力フロー

```mermaid
flowchart TD
    A[pipeToWebWritable呼び出し] --> B[writer = writable.getWriter]
    B --> C[encoder = new TextEncoder]
    C --> D{writer.readyがPromiseか判定}
    D --> E[SimpleReadableアダプター構築]
    E --> F[renderToSimpleStream呼び出し]
    F --> G{async push content}
    G -->|hasReady| H[await writer.ready]
    H --> I{content判定}
    G -->|!hasReady| I
    I -->|content != null| J[writer.write encoder.encode content]
    I -->|content == null| K[writer.close]
    F -->|エラー| L[console.log err]
    L --> M[writer.close]
    J --> G
    K --> N[終了]
    M --> N
```

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 表示メッセージ | 対処方法 |
|----------|---------|--------------|---------|
| レンダリングエラー | コンポーネントのsetupやrender中に例外が発生 | console.logにエラーオブジェクトが出力される | writer.close()でストリームを閉じる。TODOコメントあり（208行目：better error handling?） |
| writer.ready非対応 | CloudFlare Workers等でwriter.readyが実装されていない | エラーにはならない（try-catchで吸収） | hasReadyフラグをfalseにしてawait writer.readyをスキップ |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定データ件数 | コンポーネントツリーのサイズに依存 |
| 目標出力時間 | アプリケーション規模に依存（ストリーミングによりTTFBを最小化） |
| 同時出力数上限 | サーバー/Edge Runtimeリソースに依存（関数自体に制限なし） |

## セキュリティ考慮事項

- SSRContextは信頼できるサーバーサイドコードからのみアクセスされる前提
- HTMLエスケープはレンダリング処理（renderComponentVNode内）で適切に行われる
- Web WritableStreamの権限管理はランタイム環境側の責任

## 備考

- `pipeToWebWritable` はCloudFlare Workers等のWeb Streams環境向けに設計されている
- contextパラメータのデフォルト値は `{}` である（185行目）
- writer.readyプロパティの互換性対応がtry-catchで行われている（191-194行目）。これはCloudFlare Workersで `writer.ready` プロパティが実装されていない問題（GitHub Issue #4287）への対応
- エラー処理にTODOコメントが残されている（208行目: "TODO better error handling?"）。現在はconsole.logでエラーを出力してwriter.close()を呼ぶのみ
- push関数はasyncで定義されており、hasReadyがtrueの場合にawait writer.readyで書き込み準備完了を待つ（197-200行目）

---

## コードリーディングガイド

本帳票を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

Web WritableStreamとSimpleReadableインターフェースの関係を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | renderToStream.ts | `packages/server-renderer/src/renderToStream.ts` | SimpleReadableインターフェース（16-19行目）。push/destroyの2メソッド |

**読解のコツ**: pipeToWebWritableはpipeToNodeWritableと同様のアダプターパターンだが、Web WritableStream固有の処理として、(1) writable.getWriter()でWritableStreamDefaultWriterを取得、(2) TextEncoderで文字列をバイト列に変換、(3) writer.readyの互換性対応、という3つの追加ステップがある。

#### Step 2: エントリーポイントを理解する

pipeToWebWritable関数の実装を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | renderToStream.ts | `packages/server-renderer/src/renderToStream.ts` | pipeToWebWritable関数（182-214行目） |

**主要処理フロー**:
1. **187行目**: writable.getWriter()でWritableStreamDefaultWriterを取得
2. **188行目**: new TextEncoder()でエンコーダーを生成
3. **191-194行目**: writer.readyがPromiseかどうかをtry-catchで判定。CloudFlare Workers互換性対応
4. **196行目**: renderToSimpleStreamを呼び出し、SimpleReadableアダプターを渡す
5. **197-205行目**: async push(content)の実装。hasReadyならawait writer.ready、contentがnullでなければwriter.write(encoder.encode(content))、nullならwriter.close()
6. **206-212行目**: destroy(err)の実装。console.log(err)でエラー出力後、writer.close()

#### Step 3: writer.ready互換性対応を理解する

CloudFlare Workers固有の問題とその対応を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | renderToStream.ts | `packages/server-renderer/src/renderToStream.ts` | 190-194行目のwriter.ready判定ロジック |

**主要処理フロー**:
- **190行目**: `let hasReady = false` で初期化
- **192行目**: `hasReady = isPromise(writer.ready)` でready有無を判定
- **193行目**: `catch (e: any) {}` でwriter.readyアクセス時の例外を吸収
- **198-200行目**: `if (hasReady) { await writer.ready }` でreadyがある場合のみ待機

#### Step 4: renderToWebStreamとの比較

同じWeb Streams系のAPIであるrenderToWebStreamとの違いを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | renderToStream.ts | `packages/server-renderer/src/renderToStream.ts` | renderToWebStream関数（145-180行目）。ReadableStreamを新規作成する方式 |

**主要処理フロー**:
- renderToWebStream: 新しいReadableStreamを作成して返す（消費者が制御）
- pipeToWebWritable: 既存のWritableStreamに書き込む（生産者が制御）

### プログラム呼び出し階層図

```
pipeToWebWritable(input, context, writable)
    │
    ├─ writable.getWriter() → writer
    ├─ new TextEncoder() → encoder
    ├─ isPromise(writer.ready) → hasReady
    │
    └─ renderToSimpleStream(input, context, adapter)
           │
           │  adapter = {
           │    async push(content) {
           │      [hasReady] await writer.ready
           │      content != null
           │        → writer.write(encoder.encode(content))
           │      content == null
           │        → writer.close()
           │    },
           │    destroy(err) {
           │      → console.log(err)
           │      → writer.close()
           │    }
           │  }
           │
           ├─ renderComponentVNode(vnode)
           │      └─ SSRBuffer生成
           │
           ├─ unrollBuffer(buffer, adapter)
           │      └─ adapter.push(chunk) → writer.write(encoded)
           │
           ├─ resolveTeleports(context)
           │
           ├─ adapter.push(null) → writer.close()
           │
           └─ [catch] adapter.destroy(error)
                  → console.log(error)
                  → writer.close()
```

### データフロー図

```
[入力]                    [処理]                           [出力]

App | VNode ──────▶ pipeToWebWritable ───────▶ Web WritableStream
                        │                              │
SSRContext ─────────▶    ├─ renderToSimpleStream        ├─ writer.write(
                        │     └─ SimpleReadable         │     encoder.encode(chunk))
WritableStream ────▶    │        アダプター経由           │
                        │                              ├─ writer.close()
                        ├─ TextEncoder                 │
                        │     └─ string→Uint8Array     └─ console.log(err)
                        │                                  + writer.close()
                        └─ writer.ready互換性対応
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| renderToStream.ts | `packages/server-renderer/src/renderToStream.ts` | ソース | pipeToWebWritable関数（182-214行目）、renderToSimpleStream関数、SimpleReadableインターフェース定義 |
| render.ts | `packages/server-renderer/src/render.ts` | ソース | SSRBuffer/SSRContext型定義、renderComponentVNode関数 |
| renderToString.ts | `packages/server-renderer/src/renderToString.ts` | ソース | resolveTeleports関数 |
| index.ts | `packages/server-renderer/src/index.ts` | ソース | pipeToWebWritableのpublicエクスポート（11行目） |
