SvelteKitでServer-Sent Eventsを使ってリアルタイム通知を実装

こんにちは！SvelteKitはリアルタイム機能の実装に優れたフレームワークですが、WebSocketやSupabase以外の方法として、**Server-Sent Events（SSE）**が注目されています。この記事では、SvelteKitでSSEを使って、ブログに新しい記事が投稿されたときのリアルタイム通知を実装する方法を解説します。コード例を交えながら、シンプルかつ軽量な通知システムを作りましょう！

---

## Server-Sent Events（SSE）とは？

SSEは、サーバーからクライアントに一方向のデータ送信を行う技術です。WebSocketと異なり、以下のような特徴があります：
- **軽量**：双方向通信が不要な場合に最適。
- **シンプル**：HTTPプロトコルを利用し、特別なサーバー設定が不要。
- **ブラウザ対応**：モダンブラウザで広くサポート。

ブログの新着記事通知のようなシナリオでは、SSEは理想的です。ReactではSSEを扱うのにカスタムフックが必要ですが、SvelteKitではストアと組み合わせることで簡単に実装できます。

---

## プロジェクトのセットアップ

新しいSvelteKitプロジェクトを作成：

```bash
npm create svelte@latest sse-blog
cd sse-blog
npm install
```

**Skeleton UI**を追加してUIをモダンに：

```bash
npm install @skeletonlabs/skeleton @skeletonlabs/tw-plugin --save-dev
```

---

## SSEの実装

### 1. 通知用APIルートの作成
SSE用のエンドポイントを作成します。`src/routes/api/notify/+server.ts`：

```ts
import { json } from '@sveltejs/kit';

export async function GET() {
  const headers = {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive'
  };

  const stream = new ReadableStream({
    async start(controller) {
      // 模擬的な新着記事イベント
      const sendEvent = (data: { id: string; title: string }) => {
        controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
      };

      // 10秒ごとに仮の通知を送信
      const interval = setInterval(() => {
        sendEvent({
          id: crypto.randomUUID(),
          title: `新しい記事: ${new Date().toLocaleTimeString()}`
        });
      }, 10000);

      // クライアントが切断したとき
      return () => clearInterval(interval);
    }
  });

  return new Response(stream, { headers });
}
```

- **解説**：
  - `text/event-stream`でSSEを有効化。
  - `ReadableStream`で継続的なデータ送信をシミュレート。
  - 10秒ごとに仮の新着記事通知を送信（実際のアプリではデータベースの更新を監視）。

### 2. クライアント側の通知受信
通知を表示するコンポーネントを作成。`src/lib/components/Notification.svelte`：

```svelte
<script>
  import { onMount } from 'svelte';
  import { fade } from 'svelte/transition';
  import { Toast } from '@skeletonlabs/skeleton';

  let notifications = [];

  onMount(() => {
    const eventSource = new EventSource('/api/notify');

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      notifications = [data, ...notifications.slice(0, 4)]; // 最新5件のみ保持
    };

    eventSource.onerror = () => {
      eventSource.close();
    };

    return () => eventSource.close();
  });
</script>

<div class="fixed bottom-4 right-4 space-y-2">
  {#each notifications as notification (notification.id)}
    <div transition:fade={{ duration: 300 }}>
      <Toast background="variant-filled-primary">
        <h4 class="h4">{notification.title}</h4>
      </Toast>
    </div>
  {/each}
</div>
```

### 3. 通知をホームページに統合
`src/routes/+page.svelte`を更新：

```svelte
<script>
  import Notification from '$lib/components/Notification.svelte';
  import { Card } from '@skeletonlabs/skeleton';
</script>

<div class="container mx-auto p-8">
  <Card class="p-6">
    <h1 class="h1 mb-4">SvelteKitブログ</h1>
    <p class="text-lg">新しい記事が投稿されると、リアルタイムで通知が表示されます！</p>
  </Card>
  <Notification />
</div>
```

### 4. 動作確認
プロジェクトを起動：

```bash
npm run dev
```

`http://localhost:5173`を開くと、10秒ごとに**Skeleton UI**の`Toast`コンポーネントで新着記事通知が表示されます。通知は右下にフェードインし、最新5件まで保持されます。

ReactでSSEを実装する場合、**useEffect**やカスタムフックで状態を管理する必要があります：

```jsx
useEffect(() => {
  const source = new EventSource('/api/notify');
  source.onmessage = (e) => setNotifications((prev) => [...prev, JSON.parse(e.data)]);
  return () => source.close();
}, []);
```

Svelteでは、ストアと`onMount`を使ったシンプルな記述で、状態管理が直感的です。

---

## やってみよう！（チャレンジ）
データベース（例：SQLite）を監視して、実際の新着記事を通知するようにしてみましょう！第4回のDrizzle ORMを使って、以下のように変更：

```ts
import { db } from '$lib/db';
import { posts } from '$lib/db/schema';

export async function GET() {
  const stream = new ReadableStream({
    async start(controller) {
      // 定期的に新着記事をチェック
      setInterval(async () => {
        const newPosts = await db.select().from(posts).limit(1).orderBy(desc(posts.createdAt));
        if (newPosts.length) {
          controller.enqueue(`data: ${JSON.stringify(newPosts[0])}\n\n`);
        }
      }, 5000);
    }
  });
  return new Response(stream, { headers });
}
```

また、通知をクリックして該当記事に遷移する機能を追加してみてください！

---

## まとめ
この記事では、SvelteKitと**Server-Sent Events**を使って、リアルタイム通知を実装しました。**Skeleton UI**の`Toast`でモダンな通知UIを簡単に作り、Svelteのストアで状態管理をシンプルに保ちました。ReactのSSE実装と比べ、SvelteKitは少ないコードで直感的なリアルタイム機能を実現します。

この記事が役に立ったと思ったら、**LGTM**や**ストック**していただけると嬉しいです！SSEで試したいアイデアや質問があれば、コメントで教えてください。また会いましょう！