branch:
ConnectionsDemo.tsx
8681 bytesRaw
import { useAgent } from "agents/react";
import { useState } from "react";
import { Button, Input, Surface, Empty, Text } from "@cloudflare/kumo";
import { DemoWrapper } from "../../layout";
import {
  LogPanel,
  ConnectionStatus,
  CodeExplanation,
  type CodeSection
} from "../../components";
import { useLogs, useUserId, useToast } from "../../hooks";
import type {
  ConnectionsAgent,
  ConnectionsAgentState
} from "./connections-agent";

const codeSections: CodeSection[] = [
  {
    title: "Lifecycle hooks for connections",
    description:
      "Override onConnect and onClose to react when clients join or leave. Use this.getConnections() to see all active WebSocket connections.",
    code: `import { Agent, type Connection } from "agents";

class ConnectionsAgent extends Agent<Env> {
  onConnect(connection: Connection) {
    const count = [...this.getConnections()].length;
    this.broadcast(JSON.stringify({
      type: "connection_count",
      count,
    }));
  }

  onClose(connection: Connection) {
    const count = [...this.getConnections()].length;
    this.broadcast(JSON.stringify({
      type: "connection_count",
      count,
    }));
  }
}`
  },
  {
    title: "Broadcast to all clients",
    description:
      "Call this.broadcast() to send a message to every connected WebSocket client at once. Pass an array of connection IDs as the second argument to exclude specific clients — useful for avoiding echoing a message back to the sender.",
    code: `import { Agent, callable, getCurrentAgent } from "agents";

  @callable()
  broadcastMessage(message: string) {
    // Send to everyone
    this.broadcast(JSON.stringify({
      type: "broadcast",
      message,
    }));
  }

  // Exclude the sender
  @callable()
  broadcastToOthers(message: string) {
    const { connection } = getCurrentAgent();
    this.broadcast(
      JSON.stringify({ type: "broadcast", message }),
      [connection.id] // exclude this connection
    );
  }`
  }
];

export function ConnectionsDemo() {
  const userId = useUserId();
  const { logs, addLog, clearLogs } = useLogs();
  const { toast } = useToast();
  const [connectionCount, setConnectionCount] = useState(0);
  const [broadcastMessage, setBroadcastMessage] = useState(
    "Hello from the playground!"
  );
  const [receivedMessages, setReceivedMessages] = useState<
    Array<{ message: string; timestamp: number }>
  >([]);

  const agent = useAgent<ConnectionsAgent, ConnectionsAgentState>({
    agent: "connections-agent",
    name: `connections-demo-${userId}`,
    onOpen: () => {
      addLog("info", "connected");
      refreshConnectionCount();
    },
    onClose: () => addLog("info", "disconnected"),
    onError: () => addLog("error", "error", "Connection error"),
    onMessage: (event: MessageEvent) => {
      try {
        const data = JSON.parse(event.data as string);
        if (data.type === "connection_count") {
          setConnectionCount(data.count);
          addLog("in", "connection_count", data.count);
        } else if (data.type === "broadcast") {
          addLog("in", "broadcast", data.message);
          setReceivedMessages((prev) =>
            [
              ...prev,
              { message: data.message, timestamp: data.timestamp }
            ].slice(-10)
          );
        }
      } catch {
        // Not JSON
      }
    }
  });

  const refreshConnectionCount = async () => {
    try {
      const count = await agent.call("getConnectionCount");
      setConnectionCount(count);
    } catch {
      // Ignore
    }
  };

  const handleBroadcast = async () => {
    if (!broadcastMessage.trim()) return;
    addLog("out", "broadcastMessage", broadcastMessage);
    try {
      await agent.call("broadcastMessage", [broadcastMessage]);
      addLog("in", "broadcast_sent");
      toast("Broadcast sent", "info");
    } catch (e) {
      addLog("error", "error", e instanceof Error ? e.message : String(e));
    }
  };

  const openNewTab = () => {
    window.open(window.location.href, "_blank");
  };

  return (
    <DemoWrapper
      title="Connections"
      description={
        <>
          Agents can track every connected WebSocket client. Override{" "}
          <code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
            onConnect
          </code>{" "}
          and{" "}
          <code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
            onClose
          </code>{" "}
          to react when clients join or leave, use{" "}
          <code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
            this.getConnections()
          </code>{" "}
          to enumerate them, and{" "}
          <code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
            this.broadcast()
          </code>{" "}
          to send a message to everyone at once. Open this page in another tab
          and watch the count update.
        </>
      }
      statusIndicator={
        <ConnectionStatus
          status={
            agent.readyState === WebSocket.OPEN ? "connected" : "connecting"
          }
        />
      }
    >
      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
        {/* Controls */}
        <div className="space-y-6">
          {/* Connection Count */}
          <Surface className="p-4 rounded-lg ring ring-kumo-line">
            <div className="mb-4">
              <Text variant="heading3">Connected Clients</Text>
            </div>
            <div className="text-4xl font-bold text-kumo-default mb-4">
              {connectionCount}
            </div>
            <p className="text-sm text-kumo-subtle mb-4">
              Open multiple tabs to see the count update in real-time
            </p>
            <Button variant="secondary" onClick={openNewTab}>
              Open New Tab
            </Button>
          </Surface>

          {/* Broadcast */}
          <Surface className="p-4 rounded-lg ring ring-kumo-line">
            <div className="mb-4">
              <Text variant="heading3">Broadcast Message</Text>
            </div>
            <p className="text-sm text-kumo-subtle mb-3">
              Send a message to all connected clients (including yourself)
            </p>
            <div className="flex gap-2">
              <Input
                aria-label="Broadcast message"
                type="text"
                value={broadcastMessage}
                onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                  setBroadcastMessage(e.target.value)
                }
                onKeyDown={(e: React.KeyboardEvent) =>
                  e.key === "Enter" && handleBroadcast()
                }
                className="flex-1"
                placeholder="Message to broadcast"
              />
              <Button variant="primary" onClick={handleBroadcast}>
                Broadcast
              </Button>
            </div>
          </Surface>

          {/* Received Messages */}
          <Surface className="p-4 rounded-lg ring ring-kumo-line">
            <div className="mb-4">
              <Text variant="heading3">Received Broadcasts</Text>
            </div>
            {receivedMessages.length === 0 ? (
              <Empty title="No messages received yet" size="sm" />
            ) : (
              <div className="space-y-2 max-h-60 overflow-y-auto">
                {receivedMessages.map((msg, i) => (
                  <div
                    key={i}
                    className="py-2 px-3 bg-kumo-elevated rounded text-sm"
                  >
                    <div className="text-kumo-default">{msg.message}</div>
                    <div className="text-xs text-kumo-inactive">
                      {new Date(msg.timestamp).toLocaleTimeString()}
                    </div>
                  </div>
                ))}
              </div>
            )}
          </Surface>

          {/* Tips */}
          <Surface className="p-4 rounded-lg bg-kumo-elevated">
            <div className="mb-2">
              <Text variant="heading3">Try this:</Text>
            </div>
            <ol className="text-sm text-kumo-subtle space-y-1 list-decimal list-inside">
              <li>Open this page in another browser tab</li>
              <li>Watch the connection count update</li>
              <li>Send a broadcast message from one tab</li>
              <li>See it appear in all tabs</li>
            </ol>
          </Surface>
        </div>

        {/* Logs */}
        <div className="space-y-6">
          <LogPanel logs={logs} onClear={clearLogs} maxHeight="400px" />
        </div>
      </div>

      <CodeExplanation sections={codeSections} />
    </DemoWrapper>
  );
}