# 通知設計書 10-クライアント切断通知

## 概要

本ドキュメントは、Bun の DevServer 機能において、クライアント（ブラウザ）が WebSocket 接続を切断した際に、Inspector（デバッガ）に対して送信される切断通知の設計を記載する。

### 本通知の処理概要

この通知は、ブラウザクライアントが DevServer との WebSocket 接続を切断した際（タブを閉じた、ページ遷移した、ネットワーク切断など）に、Inspector フロントエンド（Bun のデバッガツール）に対してクライアント切断を通知するサーバーサイドの内部通知である。これにより、デバッグツールで接続中のクライアント一覧を正確に維持できる。

**業務上の目的・背景**：開発者がデバッガを使用している場合、どのブラウザタブ/ウィンドウが DevServer に接続しているかを正確に把握することは重要である。`notifyClientDisconnected` によりクライアント切断を Inspector に通知することで、デバッグツールがアクティブなクライアント一覧から切断されたクライアントを削除し、正確な状態を表示できるようにする。

**通知の送信タイミング**：`HmrSocket.onClose` 関数内で、WebSocket 接続が閉じられた際に送信される。`inspector_connection_id > -1`（接続時に通知が送信された）の場合のみ通知される。

**通知の受信者**：Inspector フロントエンド（Bun のデバッガ UI）。Inspector が有効な場合（`dev.inspector()` が non-null）のみ通知が送信される。

**通知内容の概要**：DevServer ID と接続 ID（connection ID）が通知される。接続 ID は `HmrSocket.inspector_connection_id` に保存された、接続時に割り当てられた識別子。

**期待されるアクション**：Inspector フロントエンドがクライアント接続リストを更新し、切断されたクライアントを一覧から削除する。

## 通知種別

内部通知（Inspector プロトコル）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 同期（C++ 関数呼び出し） |
| 優先度 | 中（デバッグ機能） |
| リトライ | 無 |

### 送信先決定ロジック

- `inspector_connection_id > -1` の場合のみ通知が送信される
- これは、接続時に `notifyClientConnected` が正常に呼び出された場合を意味する
- Inspector が無効な場合（`dev.inspector()` が null）は通知はスキップされる

## 通知テンプレート

### 関数シグネチャ

```zig
pub fn notifyClientDisconnected(
    this: *const BunFrontendDevServerAgent,
    devServerId: DebuggerId,
    connectionId: i32
) void
```

### C++ バインディング

```cpp
extern "c" fn InspectorBunFrontendDevServerAgent__notifyClientDisconnected(
    agent: *InspectorBunFrontendDevServerAgentHandle,
    devServerId: i32,
    connectionId: i32
) void
```

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| `devServerId` | DevServer の識別子 | `dev.inspector_server_id` | Yes |
| `connectionId` | クライアント接続の識別子 | `s.inspector_connection_id` | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| WebSocket 切断 | `HmrSocket.onClose` | `inspector_connection_id > -1` かつ Inspector 有効 | クライアントが切断したとき |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| Inspector 無効 | `dev.inspector()` が null の場合 |
| 接続 ID 未設定 | `inspector_connection_id == -1` の場合（接続時に通知されなかった） |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[クライアント WebSocket 切断] --> B[HmrSocket.onClose 呼び出し]
    B --> C[onUnsubscribe 呼び出し]
    C --> D{inspector_connection_id > -1?}
    D -->|No| E[通知スキップ]
    D -->|Yes| F{Inspector 有効?}
    F -->|No| G[通知スキップ]
    F -->|Yes| H[notifyClientDisconnected 呼び出し]
    H --> I[C++ バインディング経由で通知]
    I --> J[active_route クリーンアップ]
    E --> J
    G --> J
    J --> K[referenced_source_maps クリーンアップ]
    K --> L[HmrSocket 破棄]
```

## データベース参照・更新仕様

### 参照テーブル一覧

該当なし（メモリ上のデータ構造を使用）

### サーバーサイドデータ構造

| 変数名 | 型 | 用途 | 定義場所 |
|--------|------|------|---------|
| `inspector_connection_id` | `i32` | クライアント接続 ID（-1 は未設定） | `HmrSocket` |
| `inspector_server_id` | `DebuggerId` | DevServer ID | `DevServer` |
| `active_route` | `RouteBundle.Index.Optional` | アクティブルート追跡 | `HmrSocket` |
| `referenced_source_maps` | `std.AutoHashMapUnmanaged(...)` | 参照中のソースマップ | `HmrSocket` |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| Inspector ハンドル null | Inspector が初期化されていない | 通知をスキップ |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 該当なし |
| リトライ間隔 | 該当なし |
| リトライ対象エラー | 該当なし |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし |
| 1日あたり上限 | 制限なし |

### 配信時間帯

制限なし（開発時に随時発生）

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

- この通知は開発環境でのみ使用されることを想定している
- Inspector は通常 localhost からのみアクセス可能
- 接続 ID は接続時に割り当てられた識別子をそのまま使用
- 本番環境では Inspector 機能自体が無効化されるべきである

## 備考

- 切断処理では、Inspector 通知以外にも以下のクリーンアップが行われる:
  - `active_route` の視聴者カウントをデクリメント
  - `referenced_source_maps` の参照解除と破棄
  - `active_websocket_connections` からの削除
  - `HmrSocket` インスタンスの破棄
- `onUnsubscribe` は切断前にサブスクリプションの解除処理を行う

---

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

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

### 推奨読解順序

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

切断処理に関連するデータ構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | HmrSocket.zig | `src/bake/DevServer/HmrSocket.zig` | 3-12行目の `HmrSocket` 構造体フィールドを理解する |
| 1-2 | HmrSocket.zig | `src/bake/DevServer/HmrSocket.zig` | 12行目の `inspector_connection_id: i32 = -1` で接続 ID 管理を理解する |

**読解のコツ**: `HmrSocket` は WebSocket 接続ごとに作成され、切断時に破棄される。`inspector_connection_id` が `-1` 以外の場合のみ切断通知が必要。

#### Step 2: 切断処理を理解する

WebSocket 切断時の処理フローを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | HmrSocket.zig | `src/bake/DevServer/HmrSocket.zig` | 237-262行目の `onClose` 関数で切断処理を理解する |

**主要処理フロー**:
1. **242行目**: `s.onUnsubscribe(s.subscriptions)` でサブスクリプション解除
2. **244行目**: `if (s.inspector_connection_id > -1)` で接続 ID チェック
3. **246行目**: `if (s.dev.inspector()) |agent|` で Inspector 取得
4. **247行目**: `agent.notifyClientDisconnected()` で通知
5. **251-253行目**: `active_route` クリーンアップ
6. **255-259行目**: `referenced_source_maps` クリーンアップ
7. **260行目**: `active_websocket_connections` から削除
8. **261行目**: `HmrSocket` 破棄

#### Step 3: 通知関数の実装を理解する

`notifyClientDisconnected` 関数の実装を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | InspectorBunFrontendDevServerAgent.zig | `src/bun.js/api/server/InspectorBunFrontendDevServerAgent.zig` | 44-48行目の `notifyClientDisconnected` 関数 |
| 3-2 | InspectorBunFrontendDevServerAgent.zig | `src/bun.js/api/server/InspectorBunFrontendDevServerAgent.zig` | 4行目の extern 宣言で C++ バインディングを理解 |

**主要処理フロー**:
- **45行目**: `if (this.handle) |handle|` で Inspector ハンドルチェック
- **46行目**: `handle.notifyClientDisconnected(devServerId.get(), connectionId)` で C++ 関数呼び出し

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

```
クライアント WebSocket 切断
    │
    ▼
HmrSocket.onClose(ws, exit_code, message)
    │
    ├─ onUnsubscribe(subscriptions)
    │      └─ HmrTopic のサブスクリプション解除
    │
    ├─ if (inspector_connection_id > -1)
    │      │
    │      └─ if (dev.inspector()) |agent|
    │             │
    │             └─ agent.notifyClientDisconnected(server_id, connection_id)
    │                    │
    │                    └─ handle.notifyClientDisconnected() [C++ 呼び出し]
    │                           │
    │                           └─ Inspector フロントエンドに通知
    │
    ├─ if (active_route.unwrap()) |old|
    │      └─ routeBundlePtr(old).active_viewers -= 1
    │
    ├─ referenced_source_maps の参照解除
    │
    ├─ active_websocket_connections.remove(s)
    │
    └─ allocator.destroy(s) [HmrSocket 破棄]
```

### データフロー図

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

クライアント
WebSocket切断 ─────────▶ HmrSocket.onClose()
                              │
                              ▼
                    onUnsubscribe() サブスク解除
                              │
                              ▼
                    inspector_connection_id チェック
                              │
                    ┌─────────┴─────────┐
                    ▼                   ▼
              [-1 (未設定)]        [> -1 (設定済)]
                    │                   │
                    ▼                   ▼
              通知スキップ      dev.inspector() チェック
                                        │
                              ┌─────────┴─────────┐
                              ▼                   ▼
                         [null]              [agent]
                              │                   │
                              ▼                   ▼
                        通知スキップ    notifyClientDisconnected()
                                                │
                                                ▼
                                        C++ バインディング
                                                │
                                                ▼
                                     Inspector フロントエンド通知
                                                │
                                                ▼
                                        クリーンアップ処理
                                         - active_route
                                         - source_maps
                                         - HmrSocket 破棄
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| HmrSocket.zig | `src/bake/DevServer/HmrSocket.zig` | ソース | WebSocket 切断処理、`onClose` 関数 |
| InspectorBunFrontendDevServerAgent.zig | `src/bun.js/api/server/InspectorBunFrontendDevServerAgent.zig` | ソース | `notifyClientDisconnected` 関数 |
| InspectorBunFrontendDevServerAgent.cpp | `src/bun.js/bindings/InspectorBunFrontendDevServerAgent.cpp` | ソース | C++ バインディング実装 |
| InspectorBunFrontendDevServerAgent.h | `src/bun.js/bindings/InspectorBunFrontendDevServerAgent.h` | ソース | C++ ヘッダー |
| DevServer.zig | `src/bake/DevServer.zig` | ソース | `inspector_server_id`, `active_websocket_connections` 管理 |
