# chat-sync-module

チャットアプリ向けの **通信・状態管理モジュール**（TypeScript / 依存ライブラリなし）。

| 要件 | 担当クラス | 仕組み |
|---|---|---|
| 楽観的UI更新 | `MessageStore` + `ChatClient.send()` | 送信ボタン押下と同時に `pending` として一覧へ載せ、確定後は同じオブジェクトを `sent` に更新 |
| 送信リトライキュー（指数バックオフ） | `SendQueue` + `ExponentialBackoff` | FIFO で 1 件ずつ直列送信。失敗時は 1s→2s→4s…（ジッタ付き）で再送。オフライン中は待機、復帰で即再開 |
| 順序保証 | `SendQueue`（送信順） + `MessageStore`（表示順） | 先頭が成功するまで後続を送らない。表示はサーバー採番 `seq` 昇順、未確定分は末尾 |
| 重複排除（Idempotency） | `MessageStore.applyServerMessage()` | `clientId`（冪等キー）と `serverId` の二重インデックス。send 応答／プッシュ／再取得のどこから来ても 1 件に収束 |
| 通信復旧時の同期 | `ChatClient` | 復旧時に `fetchSince(lastSeq)` で取りこぼしを補完 → その後キューを再開。`seq` の飛びを検知したら自動で穴埋め |

## 実行

```powershell
cd $HOME\Desktop\chat-sync-module
npm install
npm run demo        # オフライン→復旧→順序/重複検証のシナリオを実行
npm test            # ユニットテスト 11 件
npm run typecheck   # tsc --noEmit
```

## 構成

```
src/
  types.ts              共通型・ChatTransport インターフェース・TransportError
  Emitter.ts            最小イベントエミッター
  ExponentialBackoff.ts 指数バックオフ計算（initial/max/factor/jitter/maxAttempts）
  MessageStore.ts       メッセージ状態・並び順・重複排除
  SendQueue.ts          送信リトライキュー（永続化 QueueStorage 付き）
  ChatClient.ts         UI が触るファサード。上記を配線し、復旧時の再同期を担当
  MockTransport.ts      疑似サーバー付きインメモリ通信層（テスト／デモ用）
  index.ts              公開エクスポート
examples/demo.ts        動作デモ
tests/chat.test.ts      node:test によるテスト
```

## 使い方（最小）

```ts
import { ChatClient, LocalStorageQueueStorage } from './src';

const client = new ChatClient({
  transport: myWebSocketTransport,          // ChatTransport を実装したもの
  senderId: currentUser.id,
  queueStorage: new LocalStorageQueueStorage(), // リロードしても未送信分を失わない
});

await client.restore();          // 永続化されていた未送信分を pending として復元
await client.joinRoom('general'); // 履歴取得 + 差分同期の対象に登録

// 送信ボタン
const optimistic = client.send('general', input.value); // 同期的に返る → 即描画

// 描画（React なら useSyncExternalStore の subscribe/getSnapshot に繋ぐ）
client.subscribe(() => render(client.getMessages('general')));

// 失敗したメッセージの再送・取消
client.retry(optimistic.clientId);
client.cancel(optimistic.clientId);
```

### ChatMessage.status の遷移

```
send() ──▶ pending ──▶ sending ──▶ sent
              ▲           │
              │  retryable失敗（バックオフ待ち）
              └───────────┤
                          │ 非retryable / 上限到達
                          ▼
                        failed ──retry()──▶ pending
```

## 通信層（ChatTransport）の実装要件

`src/types.ts` の `ChatTransport` を WebSocket / HTTP などで実装します。

```ts
interface ChatTransport {
  send(payload: OutgoingPayload, signal?: AbortSignal): Promise<ServerMessage>;
  fetchSince(roomId: string, afterSeq: number): Promise<ServerMessage[]>;
  subscribe(handlers: { onMessage; onConnectionChange }): () => void;
  isOnline(): boolean;
}
```

- 恒久エラー（400 系・認証切れ等）は `new TransportError(msg, /*retryable*/ false)` を投げる。即 `failed` になりリトライしない
- 一時エラー（ネットワーク断・5xx・タイムアウト）は `retryable: true`
- `signal` を尊重すると、オフライン遷移時に進行中の送信が即中断される

### サーバー側の要件（重要）

1. **`clientId` を冪等キーとして扱う**: 同じ `clientId` の送信を 2 回受けたら、新規作成せず既存メッセージを返す（応答ロス時の再送で二重投稿にならない）
2. **部屋ごとに単調増加の `seq` を採番**して返す。表示順・差分取得の基準になる
3. `fetchSince(roomId, afterSeq)` は `seq > afterSeq` を昇順で返す
4. プッシュされる `ServerMessage` に `clientId` を含める（自分の投稿の突合に使う。別端末分は無くてもよい）

## 設計上の判断

- **in-flight は常に 1 件**（グローバル FIFO）。順序保証を最優先し、実装を単純に保つため。スループットが必要なら部屋ごとに `SendQueue` を持つ構成に拡張可能
- **未確定メッセージは常に末尾に表示**。確定した瞬間に `seq` 位置へ移動する（他ユーザーの投稿が割り込んだ場合の表示ジャンプは仕様）
- **復旧時は「差分取得 → キュー再開」の順**。他ユーザーの投稿を先に取り込んでから自分の未送信分を送るので、表示順とサーバー順が一致する
- **確定済みは格下げしない**。応答ロス中にプッシュで `sent` になったメッセージは、キューの失敗通知やリトライで `pending` に戻らず、再送もスキップされる
- **バックオフはオンライン復帰で即解除**。30 秒待ちの途中で電波が戻れば待たずに送る
- `Clock` を注入できるので、テストでは仮想時計に差し替えられる
