branch:
RoutingDemo.tsx
11859 bytesRaw
import { useAgent } from "agents/react";
import { nanoid } from "nanoid";
import { useState, useEffect } from "react";
import { Button, Input, Surface, Text, Radio } from "@cloudflare/kumo";
import { DemoWrapper } from "../../layout";
import {
LogPanel,
ConnectionStatus,
CodeExplanation,
type CodeSection
} from "../../components";
import { useLogs, useUserId } from "../../hooks";
import type { RoutingAgent, RoutingAgentState } from "./routing-agent";
const codeSections: CodeSection[] = [
{
title: "Route by agent name",
description:
"The name prop on useAgent determines which Durable Object instance you connect to. Same name = same agent. Use user IDs, session IDs, or fixed strings to control isolation.",
code: `// Per-user: each user gets their own agent
const agent = useAgent({
agent: "routing-agent",
name: \`user-\${userId}\`,
});
// Shared: everyone on the same instance
const agent = useAgent({
agent: "routing-agent",
name: "shared",
});
// Per-session: each browser tab is isolated
const agent = useAgent({
agent: "routing-agent",
name: \`session-\${sessionId}\`,
});`
},
{
title: "Custom routing with basePath",
description:
"For server-controlled routing, use basePath instead of name. Your Worker's fetch handler resolves the URL to the right Durable Object using getAgentByName().",
code: `// Client: connect via a custom URL path
const agent = useAgent({
agent: "routing-agent",
basePath: \`custom-routing/\${userId}\`,
});
// Server: resolve the path in your Worker
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname.startsWith("/custom-routing/")) {
const name = url.pathname.split("/")[2];
const agent = getAgentByName(env.RoutingAgent, name);
return agent.fetch(request);
}
}
}`
}
];
type RoutingStrategy = "per-user" | "shared" | "per-session" | "custom-path";
function getSessionId(): string {
if (typeof window === "undefined") return "session-1";
let sessionId = sessionStorage.getItem("playground-session-id");
if (!sessionId) {
sessionId = `session-${nanoid(6)}`;
sessionStorage.setItem("playground-session-id", sessionId);
}
return sessionId;
}
export function RoutingDemo() {
const initialUserId = useUserId();
const { logs, addLog, clearLogs } = useLogs();
const [userId, setUserId] = useState(initialUserId);
const [strategy, setStrategy] = useState<RoutingStrategy>("per-user");
const [connectionCount, setConnectionCount] = useState(0);
const [agentInstanceName, setAgentInstanceName] = useState<string>("");
const getAgentName = () => {
switch (strategy) {
case "per-user":
return `routing-${userId}`;
case "shared":
return "routing-shared";
case "per-session":
return `routing-${getSessionId()}`;
case "custom-path":
return `routing-${userId}`;
default:
return "routing-demo";
}
};
const currentAgentName = getAgentName();
const isCustomPath = strategy === "custom-path";
const agent = useAgent<RoutingAgent, RoutingAgentState>({
agent: "routing-agent",
name: isCustomPath ? undefined : currentAgentName,
basePath: isCustomPath ? `custom-routing/${currentAgentName}` : undefined,
onOpen: () => {
if (!isCustomPath) {
addLog("info", "connected", `Agent: ${currentAgentName}`);
setAgentInstanceName(currentAgentName);
} else {
addLog(
"info",
"connected",
`Custom path: /custom-routing/${currentAgentName}`
);
}
},
onIdentity: (name, agentType) => {
addLog("info", "identity", `Server resolved: ${agentType}/${name}`);
setAgentInstanceName(name);
},
onClose: () => addLog("info", "disconnected"),
onError: () => addLog("error", "error", "Connection error"),
onStateUpdate: (newState) => {
setConnectionCount(newState.counter);
addLog("in", "state_update", { counter: newState.counter });
}
});
useEffect(() => {
localStorage.setItem("playground-user-id", userId);
}, [userId]);
const openNewTab = () => {
window.open(window.location.href, "_blank");
};
const strategies: {
id: RoutingStrategy;
label: string;
description: string;
}[] = [
{
id: "per-user",
label: "Per-User",
description: "Each user ID gets their own agent instance"
},
{
id: "shared",
label: "Shared",
description: "All users share a single agent instance"
},
{
id: "per-session",
label: "Per-Session",
description: "Each browser session gets its own agent"
},
{
id: "custom-path",
label: "Custom Path (basePath)",
description:
"Server-side routing via a custom URL path using getAgentByName"
}
];
return (
<DemoWrapper
title="Routing Strategies"
description={
<>
The{" "}
<code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">name</code>{" "}
prop on{" "}
<code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
useAgent
</code>{" "}
determines which Durable Object instance you connect to — same name
means same agent. Use user IDs for per-user isolation, a fixed string
for a shared singleton, or session IDs for per-tab agents. For
server-controlled routing, use{" "}
<code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
basePath
</code>{" "}
instead. Switch strategies below and open multiple tabs to see the
difference.
</>
}
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 Status */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-kumo-subtle">Agent Instance:</span>
<code className="bg-kumo-control px-2 py-0.5 rounded text-xs text-kumo-default">
{agentInstanceName || "connecting..."}
</code>
</div>
<div className="flex justify-between">
<span className="text-kumo-subtle">Counter:</span>
<span className="font-bold text-lg text-kumo-default">
{connectionCount}
</span>
</div>
<Button
variant="secondary"
onClick={() => agent.call("increment")}
className="w-full"
>
Increment Counter
</Button>
</div>
</Surface>
{/* User Identity */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">Your Identity</Text>
</div>
<div className="space-y-3">
<Input
label="User ID (persisted in localStorage)"
type="text"
value={userId}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setUserId(e.target.value)
}
className="w-full"
placeholder="Enter a user ID"
/>
<div>
<span className="text-xs text-kumo-subtle block mb-1">
Session ID (auto-generated per tab)
</span>
<code className="block bg-kumo-control px-3 py-2 rounded text-sm text-kumo-default">
{getSessionId()}
</code>
</div>
</div>
</Surface>
{/* Strategy Selector */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">Routing Strategy</Text>
</div>
<Radio.Group
legend="Routing Strategy"
value={strategy}
onValueChange={(value: string) => {
setStrategy(value as RoutingStrategy);
addLog("out", "strategy_change", value);
}}
>
{strategies.map((s) => (
<Radio.Item
key={s.id}
label={`${s.label} — ${s.description}`}
value={s.id}
/>
))}
</Radio.Group>
</Surface>
{/* Multi-Tab Testing */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">Try It Out</Text>
</div>
<p className="text-sm text-kumo-subtle mb-4">
Open multiple tabs to see how different strategies affect which
clients end up on the same agent instance.
</p>
<Button variant="primary" onClick={openNewTab} className="w-full">
Open New Tab
</Button>
</Surface>
{/* Explanation */}
<Surface className="p-4 rounded-lg bg-kumo-elevated">
<div className="mb-3">
<Text variant="heading3">How It Works</Text>
</div>
<div className="text-sm text-kumo-subtle space-y-2">
<p>
<strong className="text-kumo-default">Per-User:</strong> Agent
name ={" "}
<code className="text-kumo-default">routing-{userId}</code>
<br />
<span className="text-xs">
Same user across tabs/devices shares an agent
</span>
</p>
<p>
<strong className="text-kumo-default">Shared:</strong> Agent
name = <code className="text-kumo-default">routing-shared</code>
<br />
<span className="text-xs">
Everyone connects to the same agent
</span>
</p>
<p>
<strong className="text-kumo-default">Per-Session:</strong>{" "}
Agent name ={" "}
<code className="text-kumo-default">
routing-{getSessionId()}
</code>
<br />
<span className="text-xs">
Each browser tab gets its own agent
</span>
</p>
<p>
<strong className="text-kumo-default">Custom Path:</strong>{" "}
basePath ={" "}
<code className="text-kumo-default">
/custom-routing/routing-{userId}
</code>
<br />
<span className="text-xs">
Server handles routing via{" "}
<code className="text-kumo-default">getAgentByName</code> —
client uses{" "}
<code className="text-kumo-default">basePath</code> instead of{" "}
<code className="text-kumo-default">agent</code>/
<code className="text-kumo-default">name</code>
</span>
</p>
</div>
</Surface>
</div>
{/* Logs */}
<div className="space-y-6">
<LogPanel logs={logs} onClear={clearLogs} maxHeight="600px" />
</div>
</div>
<CodeExplanation sections={codeSections} />
</DemoWrapper>
);
}