コンテンツにスキップ

通知設計

基本方針

受注者が新しいタスクにすばやく気づける通知の仕組みを設計する。 MVPでは SSE(Server-Sent Events)によるリアルタイム通知から始め、段階的に拡張する。

通知方式: SSE

比較軸SSEWebSocketポーリング
方向サーバー→クライアント(単方向)双方向クライアント→サーバー
実装コスト低い高い低い
リアルタイム性高い高い低い(間隔依存)
自動再接続ブラウザ組み込み自前実装が必要不要
HTTP/2 互換良好別コネクション良好

通知はサーバーからクライアントへの単方向で十分なため、SSE を採用する。 WebSocket の双方向通信は MVP では不要な複雑さを持ち込む。 ポーリングは遅延とサーバー負荷のバランスが悪い。

マッチングモデルとの整合

マッチング設計の「全公開 + 早い者勝ち」方式に準拠する。

  • タスクが投稿されたら 全受注者 にリアルタイム通知する
  • 受注者がフィルタなしで全タスクを受信する(MVP ではカテゴリ・位置フィルタなし)
  • 受諾は早い者勝ち。受諾済みタスクの状態変更も SSE で配信し、他の受注者の画面を更新する

SSE エンドポイント設計

GET /tasks/events

受注者向けのタスクイベントストリーム。

GET /tasks/events
Accept: text/event-stream
Authorization: Bearer <token>

レスポンス:

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
:heartbeat
event: task.created
data: {"id":"t_abc123","title":"渋谷駅前の写真撮影","reward":1500,"status":"open","createdAt":"2026-02-25T10:00:00Z"}
event: task.accepted
data: {"id":"t_abc123","status":"accepted"}

クエリパラメータ

パラメータ説明
lastEventIdstring再接続時の復帰ポイント。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_001
event: task.created
data: {"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 の組み込み機能を利用)

オフライン復帰

ブラウザがオフラインから復帰した場合:

  1. EventSource が自動再接続し Last-Event-ID を送信
  2. サーバーがリングバッファから未受信イベントをリプレイ
  3. リングバッファの範囲外(長時間オフライン)の場合はタスク一覧を 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 方針「全公開」に合わせる。フィルタリングはマッチング側の拡張と同時に導入する