通知設計
基本方針
受注者が新しいタスクにすばやく気づける通知の仕組みを設計する。 MVPでは SSE(Server-Sent Events)によるリアルタイム通知から始め、段階的に拡張する。
通知方式: SSE
| 比較軸 | SSE | WebSocket | ポーリング |
|---|---|---|---|
| 方向 | サーバー→クライアント(単方向) | 双方向 | クライアント→サーバー |
| 実装コスト | 低い | 高い | 低い |
| リアルタイム性 | 高い | 高い | 低い(間隔依存) |
| 自動再接続 | ブラウザ組み込み | 自前実装が必要 | 不要 |
| HTTP/2 互換 | 良好 | 別コネクション | 良好 |
通知はサーバーからクライアントへの単方向で十分なため、SSE を採用する。 WebSocket の双方向通信は MVP では不要な複雑さを持ち込む。 ポーリングは遅延とサーバー負荷のバランスが悪い。
マッチングモデルとの整合
マッチング設計の「全公開 + 早い者勝ち」方式に準拠する。
- タスクが投稿されたら 全受注者 にリアルタイム通知する
- 受注者がフィルタなしで全タスクを受信する(MVP ではカテゴリ・位置フィルタなし)
- 受諾は早い者勝ち。受諾済みタスクの状態変更も SSE で配信し、他の受注者の画面を更新する
SSE エンドポイント設計
GET /tasks/events
受注者向けのタスクイベントストリーム。
GET /tasks/eventsAccept: text/event-streamAuthorization: Bearer <token>レスポンス:
HTTP/1.1 200 OKContent-Type: text/event-streamCache-Control: no-cacheConnection: keep-alive
:heartbeatevent: task.createddata: {"id":"t_abc123","title":"渋谷駅前の写真撮影","reward":1500,"status":"open","createdAt":"2026-02-25T10:00:00Z"}
event: task.accepteddata: {"id":"t_abc123","status":"accepted"}クエリパラメータ
| パラメータ | 型 | 説明 |
|---|---|---|
lastEventId | string | 再接続時の復帰ポイント。Last-Event-ID ヘッダと同等 |
ブラウザの EventSource は再接続時に Last-Event-ID ヘッダを自動送信するが、一部プロキシがヘッダを落とすケースに備えてクエリパラメータでも受け付ける。
イベント種別とペイロード
task.created
新しいタスクが投稿された。受注者のタスク一覧に追加する。
{ "id": "t_abc123", "title": "渋谷駅前の写真撮影", "description": "ハチ公前の現在の混雑状況を写真3枚で撮影", "reward": 1500, "status": "open", "createdAt": "2026-02-25T10:00:00Z"}task.accepted
タスクが他の受注者に受諾された。一覧から募集中表示を消す。
{ "id": "t_abc123", "status": "accepted"}task.cancelled
タスクがキャンセルされた。一覧から削除する。
{ "id": "t_abc123", "status": "cancelled"}task.expired
タスクの受託期限が切れた。一覧から削除する。
{ "id": "t_abc123", "status": "expired"}全イベント共通で SSE の id フィールドを付与し、再接続時のリプレイに使う。
id: evt_001event: task.createddata: {"id":"t_abc123",...}サーバー側アーキテクチャ
in-memory イベントバス + リングバッファ
[API ハンドラ] --publish--> [EventBus] --fan-out--> [SSE コネクション 1] [SSE コネクション 2] [SSE コネクション N] | [リングバッファ] (直近 1000 件保持)- EventBus: Node.js の
EventEmitterベース。タスク状態変更時にイベントを発行する - リングバッファ: 直近 1000 件のイベントを保持。再接続時に
Last-Event-ID以降のイベントをリプレイする - コネクション管理: アクティブな SSE コネクションを
Map<userId, Response>で保持。切断時に自動クリーンアップ
ハートビート
30 秒ごとに SSE コメント(:heartbeat)を送信し、コネクション維持とプロキシタイムアウト回避を行う。
認証
SSE 接続時に Bearer トークンを検証する。未認証の場合は 401 を返す。
EventSource API はカスタムヘッダを設定できないため、クエリパラメータ ?token=<token> でもトークンを受け付ける。
クライアント側アーキテクチャ
EventSource フック
React カスタムフックでイベント購読を管理する。
// 概念的な API(実装時に詳細を決定)function useTaskEvents(): { tasks: Task[]; connectionStatus: 'connecting' | 'open' | 'closed';}責務:
EventSourceの生成・破棄(コンポーネントのライフサイクルに連動)- 受信イベントでローカル状態を更新
- 接続状態の管理と UI への公開
- 自動再接続(
EventSourceの組み込み機能を利用)
オフライン復帰
ブラウザがオフラインから復帰した場合:
EventSourceが自動再接続しLast-Event-IDを送信- サーバーがリングバッファから未受信イベントをリプレイ
- リングバッファの範囲外(長時間オフライン)の場合はタスク一覧を REST API でフルフェッチ
MVP スコープ
やること
- SSE エンドポイント
GET /tasks/eventsの実装 task.created/task.accepted/task.cancelled/task.expiredイベントの配信- in-memory イベントバス + リングバッファ(直近 1000 件)
Last-Event-IDによる再接続リプレイ- ハートビート送信(30 秒間隔)
- React カスタムフック
useTaskEvents
やらないこと
- Web Push 通知(ブラウザを閉じている受注者への通知)
- マルチサーバー対応(Redis Pub/Sub 等による分散イベントバス)
- イベントの永続化(DB 保存)
- カテゴリ・位置情報によるイベントフィルタリング
- 受注者個別の通知設定(通知オン/オフ、カテゴリ選択等)
- メール・SMS 通知
将来の拡張
Phase 1: Web Push 通知
ブラウザを閉じている受注者にも通知を届ける。Service Worker + Push API を使い、タスク投稿時にプッシュ通知を送信する。SSE はアプリ利用中のリアルタイム更新、Web Push はアプリ未使用時の通知として併用する。
Phase 2: マルチサーバー対応
サーバーを複数台に増やした場合、in-memory イベントバスではサーバー間でイベントを共有できない。Redis Pub/Sub をイベントバスとして導入し、全サーバーのコネクションにイベントを配信する。
Phase 3: イベントフィルタリング
マッチング設計の Phase 1〜2(カテゴリフィルタ・位置情報マッチング)と連動し、受注者の設定に基づいてサーバー側でイベントをフィルタリングする。全イベント配信からターゲット配信に移行することで、クライアントの処理負荷とデータ転送量を削減する。
設計判断の記録
| 判断 | 理由 |
|---|---|
| SSE を採用し WebSocket を使わない | 通知は単方向で十分。SSE は自動再接続・Last-Event-ID がブラウザ組み込みで、実装コストが低い |
| in-memory リングバッファで再接続対応 | MVP の単一サーバー構成では DB 永続化は過剰。1000 件あれば短時間の切断は十分カバーできる |
EventSource のクエリパラメータでもトークンを受け付ける | EventSource API はカスタムヘッダを設定できない制約への対応 |
| MVP で Web Push を入れない | Service Worker の実装・テストコストが高い。まずはアプリ利用中の通知に集中する |
| 全受注者に全イベントを配信 | マッチング設計の MVP 方針「全公開」に合わせる。フィルタリングはマッチング側の拡張と同時に導入する |