WebSocket/SSE

SvelteKitでWebSocketとServer-Sent Events (SSE)を使用したリアルタイム通信の実装方法について解説します。

リアルタイム通信の選択肢

通信方式の比較

方式双方向プロトコルユースケース
WebSocketws/wssチャット、ゲーム、コラボレーション
SSE× (サーバー→クライアント)HTTP/HTTPS通知、ライブフィード、進捗表示
Long PollingHTTP/HTTPSレガシー環境対応

Server-Sent Events (SSE)

SSEの実装

サーバー側 (+server.ts)

// src/routes/api/events/+server.ts
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async ({ request }) => {
  const stream = new ReadableStream({
    start(controller) {
      const encoder = new TextEncoder();
      
      // 初期メッセージ
      controller.enqueue(
        encoder.encode('data: {"type":"connected","timestamp":' + Date.now() + '}\n\n')
      );
      
      // 定期的にデータを送信
      const interval = setInterval(() => {
        const data = {
          type: 'update',
          timestamp: Date.now(),
          value: Math.random()
        };
        
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
        );
      }, 1000);
      
      // クライアント切断時のクリーンアップ
      request.signal.addEventListener('abort', () => {
        clearInterval(interval);
        controller.close();
      });
    }
  });
  
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive'
    }
  });
};
typescript

クライアント側 (+page.svelte)

<script lang="ts">
  import { onMount } from 'svelte';
  
  type EventData = {
    type: string;
    timestamp: number;
    value?: number;
  };
  
  let events = $state<EventData[]>([]);
  let eventSource: EventSource | null = null;
  
  onMount(() => {
    eventSource = new EventSource('/api/events');
    
    eventSource.onmessage = (event) => {
      const data: EventData = JSON.parse(event.data);
      events = [...events, data];
      
      // 最新10件のみ保持
      if (events.length > 10) {
        events = events.slice(-10);
      }
    };
    
    eventSource.onerror = (error) => {
      console.error('SSE error:', error);
      eventSource?.close();
    };
    
    return () => {
      eventSource?.close();
    };
  });
</script>

<div class="events">
  <h2>リアルタイムイベント</h2>
  {#each events as event}
    <div class="event">
      <span>{event.type}</span>
      <span>{new Date(event.timestamp).toLocaleTimeString()}</span>
      {#if event.value !== undefined}
        <span>{event.value.toFixed(3)}</span>
      {/if}
    </div>
  {/each}
</div>
svelte

WebSocket実装

Viteプラグインを使用したWebSocket

vite.config.tsの設定

import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import type { ViteDevServer } from 'vite';
import { WebSocketServer } from 'ws';

function webSocketPlugin() {
  let wss: WebSocketServer;
  
  return {
    name: 'websocket-plugin',
    configureServer(server: ViteDevServer) {
      wss = new WebSocketServer({ port: 5174 });
      
      wss.on('connection', (ws) => {
        console.log('Client connected');
        
        ws.on('message', (data) => {
          const message = JSON.parse(data.toString());
          
          // すべてのクライアントにブロードキャスト
          wss.clients.forEach((client) => {
            if (client.readyState === 1) {
              client.send(JSON.stringify({
                type: 'broadcast',
                ...message,
                timestamp: Date.now()
              }));
            }
          });
        });
        
        ws.on('close', () => {
          console.log('Client disconnected');
        });
      });
    }
  };
}

export default defineConfig({
  plugins: [sveltekit(), webSocketPlugin()]
});
typescript

クライアント側の実装

<script lang="ts">
  import { onMount } from 'svelte';
  
  type Message = {
    id: string;
    user: string;
    text: string;
    timestamp: number;
  };
  
  let messages = $state<Message[]>([]);
  let inputText = $state('');
  let username = $state('User' + Math.floor(Math.random() * 1000));
  let ws: WebSocket | null = null;
  let connected = $state(false);
  
  onMount(() => {
    ws = new WebSocket('ws://localhost:5174');
    
    ws.onopen = () => {
      connected = true;
      console.log('WebSocket connected');
    };
    
    ws.onmessage = (event) => {
      const message: Message = JSON.parse(event.data);
      messages = [...messages, message];
    };
    
    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
      connected = false;
    };
    
    ws.onclose = () => {
      connected = false;
      console.log('WebSocket disconnected');
    };
    
    return () => {
      ws?.close();
    };
  });
  
  function sendMessage() {
    if (!ws || !inputText.trim()) return;
    
    const message = {
      id: crypto.randomUUID(),
      user: username,
      text: inputText
    };
    
    ws.send(JSON.stringify(message));
    inputText = '';
  }
</script>

<div class="chat">
  <div class="status" class:connected>
    {connected ? '接続中' : '切断'}
  </div>
  
  <div class="messages">
    {#each messages as message}
      <div class="message">
        <strong>{message.user}:</strong>
        <span>{message.text}</span>
        <time>{new Date(message.timestamp).toLocaleTimeString()}</time>
      </div>
    {/each}
  </div>
  
  <form onsubmit={sendMessage}>
    <input
      bind:value={inputText}
      placeholder="メッセージを入力..."
      disabled={!connected}
    />
    <button type="submit" disabled={!connected}>
      送信
    </button>
  </form>
</div>

<style>
  .status {
    padding: 0.5rem;
    background: #f44336;
    color: white;
  }
  
  .status.connected {
    background: #4caf50;
  }
  
  .messages {
    height: 300px;
    overflow-y: auto;
    border: 1px solid #ddd;
    padding: 1rem;
  }
  
  .message {
    margin-bottom: 0.5rem;
  }
  
  time {
    font-size: 0.8em;
    color: #666;
    margin-left: 0.5rem;
  }
</style>
svelte

Socket.IOの統合

Socket.IO実装例

サーバー側設定

// vite.config.ts
import { createServer } from 'http';
import { Server } from 'socket.io';

function socketIOPlugin() {
  return {
    name: 'socket-io',
    configureServer(server: ViteDevServer) {
      const httpServer = createServer();
      const io = new Server(httpServer, {
        cors: {
          origin: 'http://localhost:5173',
          methods: ['GET', 'POST']
        }
      });
      
      io.on('connection', (socket) => {
        console.log('User connected:', socket.id);
        
        socket.on('join-room', (roomId) => {
          socket.join(roomId);
          socket.to(roomId).emit('user-joined', socket.id);
        });
        
        socket.on('leave-room', (roomId) => {
          socket.leave(roomId);
          socket.to(roomId).emit('user-left', socket.id);
        });
        
        socket.on('message', (data) => {
          io.to(data.room).emit('message', {
            ...data,
            timestamp: Date.now()
          });
        });
        
        socket.on('disconnect', () => {
          console.log('User disconnected:', socket.id);
        });
      });
      
      httpServer.listen(5174);
    }
  };
}
typescript

クライアント側の実装

<script lang="ts">
  import { onMount } from 'svelte';
  import io from 'socket.io-client';
  
  let socket: ReturnType<typeof io>;
  let currentRoom = $state('general');
  let rooms = $state(['general', 'tech', 'random']);
  
  onMount(() => {
    socket = io('http://localhost:5174');
    
    socket.on('connect', () => {
      console.log('Connected to Socket.IO');
      socket.emit('join-room', currentRoom);
    });
    
    socket.on('message', (data) => {
      // メッセージ処理
    });
    
    socket.on('user-joined', (userId) => {
      console.log('User joined:', userId);
    });
    
    return () => {
      socket.disconnect();
    };
  });
  
  function switchRoom(room: string) {
    socket.emit('leave-room', currentRoom);
    currentRoom = room;
    socket.emit('join-room', room);
  }
</script>
svelte

プロダクション環境での考慮事項

スケーラビリティ

// Redis Pub/Subを使用した水平スケーリング
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();

await Promise.all([
  pubClient.connect(),
  subClient.connect()
]);

io.adapter(createAdapter(pubClient, subClient));
typescript

認証とセキュリティ

// JWTトークンによる認証
import jwt from 'jsonwebtoken';

io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!);
    socket.data.user = decoded;
    next();
  } catch (err) {
    next(new Error('Authentication error'));
  }
});
typescript

レート制限

// レート制限の実装
const rateLimiter = new Map<string, number[]>();

function checkRateLimit(socketId: string): boolean {
  const now = Date.now();
  const timestamps = rateLimiter.get(socketId) || [];
  
  // 1分以内のリクエストをフィルタ
  const recentRequests = timestamps.filter(t => now - t < 60000);
  
  if (recentRequests.length >= 100) {
    return false; // レート制限に達した
  }
  
  recentRequests.push(now);
  rateLimiter.set(socketId, recentRequests);
  return true;
}
typescript

デプロイメント戦略

Adapterの選択

プラットフォームWebSocket対応推奨Adapter
Vercel△ (制限あり)@sveltejs/adapter-vercel
Netlify×SSEを使用
Node.js@sveltejs/adapter-node
Cloudflare○ (Durable Objects)@sveltejs/adapter-cloudflare

Node.js Adapterでの実装

// server.js (カスタムサーバー)
import { handler } from './build/handler.js';
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';

const app = express();
const server = createServer(app);
const io = new Server(server);

// Socket.IOのセットアップ
io.on('connection', (socket) => {
  // WebSocket処理
});

// SvelteKitハンドラー
app.use(handler);

server.listen(3000, () => {
  console.log('Server running on port 3000');
});
typescript

まとめ

SvelteKitでリアルタイム通信を実装する際は、要件に応じて適切な技術を選択することが重要です。SSEは単方向通信に適しており、WebSocketは双方向通信が必要な場合に使用します。プロダクション環境では、スケーラビリティとセキュリティを考慮した実装が必要です。

Last update at: 2025/08/30 12:36:35