mcpField
B
bun-websocket-server

bun-websocket-server

github.com/secondsky開発240398 回インストール5 週間前に更新
リポジトリ

bun-websocket-server について

Bunでリアルタイムサーバーを構築する開発者向けのWebSocketスキルです。Bun.serve()での接続処理、メッセージ、トピック配信、設定、認証、エラー処理を説明します。

主な機能

  1. 01HTTPリクエストのWebSocket化
  2. 02接続とメッセージの処理
  3. 03トピック単位の配信とブロードキャスト
  4. 04ペイロードとアイドル時間の設定
  5. 05アップグレード前の認証

活用シーン

  1. 01チャットルームの構築
  2. 02接続中のクライアントへの配信
  3. 03WebSocket接続への認証追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/secondsky/claude-skills --skill bun-websocket-server

インストール後、Claude Code で/bun-websocket-serverを入力して読み込まれたか確認します。

詳細

Bun WebSocket サーバー

Bun には、Bun.serve() に統合された WebSocket のサポートが組み込まれています。

クイックスタート

const server = Bun.serve({
  fetch(req, server) {
    // Upgrade to WebSocket
    if (server.upgrade(req)) {
      return; // Upgraded successfully
    }
    return new Response("Not a WebSocket request", { status: 400 });
  },
  websocket: {
    open(ws) {
      console.log("Client connected");
    },
    message(ws, message) {
      console.log("Received:", message);
      ws.send(`Echo: ${message}`);
    },
    close(ws) {
      console.log("Client disconnected");
    },
  },
});

console.log(`WebSocket server running on ws://localhost:${server.port}`);

WebSocket ハンドラー

Bun.serve({
  fetch(req, server) {
    server.upgrade(req);
  },
  websocket: {
    // Client connected
    open(ws) {
      console.log("New connection");
    },

    // Message received
    message(ws, message) {
      // message is string | Buffer
      if (typeof message === "string") {
        console.log("Text:", message);
      } else {
        console.log("Binary:", message);
      }
    },

    // Connection closed
    close(ws, code, reason) {
      console.log(`Closed: ${code} - ${reason}`);
    },

    // Drain event (buffer flushed)
    drain(ws) {
      console.log("Buffer drained");
    },

    // Ping received
    ping(ws, data) {
      // Pong sent automatically
    },

    // Pong received
    pong(ws, data) {
      console.log("Pong received");
    },
  },
});

メッセージの送信

websocket: {
  message(ws, message) {
    // Send text
    ws.send("Hello");

    // Send JSON
    ws.send(JSON.stringify({ type: "greeting", data: "Hello" }));

    // Send binary
    ws.send(new Uint8Array([1, 2, 3]));
    ws.send(Buffer.from("binary data"));

    // Send with compression
    ws.send("compressed message", true);

    // Check if buffer is full
    const bufferedAmount = ws.send("data");
    if (bufferedAmount > 1024 * 1024) {
      console.log("Buffer getting full");
    }
  },
}

接続へのデータの追加

interface UserData {
  id: string;
  name: string;
  joinedAt: Date;
}

Bun.serve<UserData>({
  fetch(req, server) {
    const url = new URL(req.url);
    const userId = url.searchParams.get("userId");

    // Attach data during upgrade
    server.upgrade(req, {
      data: {
        id: userId,
        name: "User " + userId,
        joinedAt: new Date(),
      },
    });
  },
  websocket: {
    open(ws) {
      // Access attached data
      console.log(`${ws.data.name} connected`);
    },
    message(ws, message) {
      console.log(`${ws.data.name}: ${message}`);
    },
  },
});

Pub/Sub(トピック)

Bun.serve({
  fetch(req, server) {
    const url = new URL(req.url);
    const room = url.searchParams.get("room") || "general";

    server.upgrade(req, {
      data: { room },
    });
  },
  websocket: {
    open(ws) {
      // Subscribe to a topic
      ws.subscribe(ws.data.room);

      // Publish to topic (excludes sender)
      ws.publish(ws.data.room, `User joined ${ws.data.room}`);
    },
    message(ws, message) {
      // Broadcast to all in room (excludes sender)
      ws.publish(ws.data.room, message);
    },
    close(ws) {
      // Unsubscribe (automatic on close)
      ws.unsubscribe(ws.data.room);
      ws.publish(ws.data.room, "User left");
    },
  },
});

すべてのクライアントへのブロードキャスト

Bun.serve({
  fetch(req, server) {
    server.upgrade(req);
  },
  websocket: {
    open(ws) {
      // Subscribe to global topic
      ws.subscribe("global");
    },
    message(ws, message) {
      // Broadcast to ALL clients including sender
      server.publish("global", message);
    },
  },
});

サーバーレベルのパブリッシュ

const server = Bun.serve({
  fetch(req, server) {
    const url = new URL(req.url);

    // HTTP endpoint to publish
    if (url.pathname === "/broadcast") {
      const message = url.searchParams.get("msg");
      server.publish("global", message);
      return new Response("Broadcasted");
    }

    server.upgrade(req);
  },
  websocket: {
    open(ws) {
      ws.subscribe("global");
    },
  },
});

// Can also publish from outside fetch
setInterval(() => {
  server.publish("global", `Server time: ${new Date().toISOString()}`);
}, 5000);

WebSocket のオプション

Bun.serve({
  websocket: {
    // Max message size (default 16MB)
    maxPayloadLength: 1024 * 1024, // 1MB

    // Idle timeout in seconds (default 120)
    idleTimeout: 60,

    // Backpressure limit
    backpressureLimit: 1024 * 1024,

    // Enable compression
    perMessageDeflate: true,
    // Or with options
    perMessageDeflate: {
      compress: "shared",
      decompress: "shared",
    },

    // Send/receive pings
    sendPings: true,

    // Handlers
    open(ws) {},
    message(ws, message) {},
    close(ws) {},
  },
});

クライアント側の接続

// Browser
const ws = new WebSocket("ws://localhost:3000");

ws.onopen = () => {
  ws.send("Hello Server!");
};

ws.onmessage = (event) => {
  console.log("Received:", event.data);
};

ws.onclose = () => {
  console.log("Disconnected");
};

認証

Bun.serve({
  fetch(req, server) {
    // Verify auth before upgrade
    const token = req.headers.get("Authorization");

    if (!verifyToken(token)) {
      return new Response("Unauthorized", { status: 401 });
    }

    const user = decodeToken(token);
    server.upgrade(req, {
      data: { userId: user.id },
    });
  },
  websocket: {
    open(ws) {
      console.log(`Authenticated user ${ws.data.userId} connected`);
    },
  },
});

よくあるエラー

エラー原因対処法
Upgrade failed無効なリクエストアップグレードヘッダーを確認します
Connection closedクライアントの切断close ハンドラーで処理します
Message too largemaxPayloadLength を超過上限を増やすか、データを分割します
Backpressureクライアントの応答が遅いバッファーを確認し、ドレインを待ちます

よくあるパターン

チャットルーム
Bun.serve({
  fetch(req, server) {
    const url = new URL(req.url);
    const username = url.searchParams.get("user") || "Anonymous";

    server.upgrade(req, {
      data: { username },
    });
  },
  websocket: {
    open(ws) {
      ws.subscribe("chat");
      ws.publish("chat", `${ws.data.username} joined`);
    },
    message(ws, message) {
      ws.publish("chat", `${ws.data.username}: ${message}`);
    },
    close(ws) {
      ws.publish("chat", `${ws.data.username} left`);
    },
  },
});

リファレンスを読み込むタイミング

次の場合は references/compression.md を読み込みます。

  • perMessageDeflate の設定
  • 圧縮の調整
  • バイナリメッセージの処理

次の場合は references/scaling.md を読み込みます。

  • 複数のサーバーインスタンス
  • Redis pub/sub の統合
  • 水平スケーリング

コメント