branch:
RetryDemo.tsx
10884 bytesRaw
import { useAgent } from "agents/react";
import { useState } from "react";
import { Button, Input, Surface, Text } from "@cloudflare/kumo";
import { DemoWrapper } from "../../layout";
import {
LogPanel,
ConnectionStatus,
CodeExplanation,
type CodeSection
} from "../../components";
import { useLogs, useUserId, useToast } from "../../hooks";
import type { RetryAgent, RetryAgentState } from "./retry-agent";
const codeSections: CodeSection[] = [
{
title: "Retry with this.retry()",
description:
"Wrap any fallible operation in this.retry(). It uses exponential backoff by default. Set class-level defaults with static options, or pass per-call overrides.",
code: `import { Agent, callable } from "agents";
class RetryAgent extends Agent<Env> {
static options = {
retry: { maxAttempts: 4, baseDelayMs: 50, maxDelayMs: 1000 },
};
@callable()
async retryFlaky(succeedOnAttempt: number) {
return await this.retry(async (attempt) => {
if (attempt < succeedOnAttempt) {
throw new Error(\`Transient failure on attempt \${attempt}\`);
}
return \`Success on attempt \${attempt}\`;
});
}
}`
},
{
title: "Selective retry with shouldRetry",
description:
"Pass a shouldRetry callback to decide whether a specific error is worth retrying. Return false to bail immediately on permanent failures.",
code: ` @callable()
async retryWithFilter(failCount: number, permanent: boolean) {
return await this.retry(
async (attempt) => {
if (attempt <= failCount) {
const err = new Error("failure");
err.permanent = permanent;
throw err;
}
return "success";
},
{
maxAttempts: 10,
shouldRetry: (err) => !err.permanent,
}
);
}`
},
{
title: "Queue with retry options",
description:
"this.queue() also supports retry options. The queued callback will be retried with backoff if it throws.",
code: ` @callable()
async queueWithRetry(maxAttempts: number) {
return await this.queue("onQueuedTask", { maxAttempts }, {
retry: { maxAttempts, baseDelayMs: 50, maxDelayMs: 500 },
});
}
async onQueuedTask(payload: { maxAttempts: number }) {
// Fails until last attempt, then succeeds
this._attempts++;
if (this._attempts < payload.maxAttempts) {
throw new Error("Not yet");
}
// Success!
}`
}
];
export function RetryDemo() {
const userId = useUserId();
const { logs, addLog, clearLogs } = useLogs();
const { toast } = useToast();
const [succeedOn, setSucceedOn] = useState("3");
const [failCount, setFailCount] = useState("2");
const [permanent, setPermanent] = useState(false);
const [queueAttempts, setQueueAttempts] = useState("3");
const agent = useAgent<RetryAgent, RetryAgentState>({
agent: "retry-agent",
name: `retry-demo-${userId}`,
onOpen: () => addLog("info", "connected"),
onClose: () => addLog("info", "disconnected"),
onError: () => addLog("error", "error", "Connection error"),
onMessage: (message: MessageEvent) => {
try {
const data = JSON.parse(message.data as string);
if (data.type === "log" && data.entry) {
const entry = data.entry;
const logType =
entry.type === "success"
? "in"
: entry.type === "failure"
? "error"
: entry.type === "attempt"
? "out"
: "info";
addLog(logType, entry.type, entry.message);
}
} catch {
// Not JSON
}
}
});
const handleRetryFlaky = async () => {
addLog("out", "retryFlaky", { succeedOnAttempt: Number(succeedOn) });
try {
const result = await agent.call("retryFlaky", [Number(succeedOn)]);
addLog("in", "result", result);
toast(String(result), "success");
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
toast(e instanceof Error ? e.message : String(e), "error");
}
};
const handleRetryWithFilter = async () => {
addLog("out", "retryWithFilter", {
failCount: Number(failCount),
permanent
});
try {
const result = await agent.call("retryWithFilter", [
Number(failCount),
permanent
]);
addLog("in", "result", result);
toast(String(result), "success");
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
toast(e instanceof Error ? e.message : String(e), "error");
}
};
const handleQueueRetry = async () => {
addLog("out", "queueWithRetry", {
maxAttempts: Number(queueAttempts)
});
try {
const id = await agent.call("queueWithRetry", [Number(queueAttempts)]);
addLog("in", "queued", { id });
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
}
};
const handleClear = async () => {
clearLogs();
try {
await agent.call("clearLog", []);
} catch {
// ignore
}
};
return (
<DemoWrapper
title="Retries"
description={
<>
Wrap any fallible operation in{" "}
<code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
this.retry()
</code>{" "}
to automatically retry with exponential backoff. Set class-level
defaults with{" "}
<code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
static options
</code>
, or pass per-call overrides. Use a{" "}
<code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
shouldRetry
</code>{" "}
callback to bail early on permanent errors. Queued tasks and scheduled
tasks also support retry options.
</>
}
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">
{/* this.retry() — Flaky Operation */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">this.retry() — Flaky Operation</Text>
</div>
<p className="text-sm text-kumo-subtle mb-3">
Simulates a flaky operation that succeeds on the Nth attempt. Uses
class-level defaults (4 max attempts).
</p>
<div className="space-y-2">
<div className="flex gap-2 items-center">
<span className="text-sm text-kumo-subtle">
Succeed on attempt
</span>
<Input
aria-label="Succeed on attempt number"
type="number"
value={succeedOn}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setSucceedOn(e.target.value)
}
className="w-20"
min={1}
max={10}
/>
</div>
<Button
variant="primary"
onClick={handleRetryFlaky}
className="w-full"
>
Run Flaky Operation
</Button>
</div>
</Surface>
{/* shouldRetry — Selective Retry */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">shouldRetry — Selective Retry</Text>
</div>
<p className="text-sm text-kumo-subtle mb-3">
Uses shouldRetry to bail early on permanent errors. Transient
errors are retried; permanent errors stop immediately.
</p>
<div className="space-y-2">
<div className="flex gap-2 items-center">
<span className="text-sm text-kumo-subtle">
Failures before success
</span>
<Input
aria-label="Number of failures"
type="number"
value={failCount}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setFailCount(e.target.value)
}
className="w-20"
min={1}
max={9}
/>
</div>
<label className="flex items-center gap-2 text-sm text-kumo-subtle cursor-pointer">
<input
type="checkbox"
checked={permanent}
onChange={(e) => setPermanent(e.target.checked)}
/>
Permanent error (shouldRetry returns false)
</label>
<Button
variant="primary"
onClick={handleRetryWithFilter}
className="w-full"
>
Run Filtered Retry
</Button>
</div>
</Surface>
{/* Queue with Retry */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">Queue with Retry</Text>
</div>
<p className="text-sm text-kumo-subtle mb-3">
Queues a task that fails until the last retry attempt, then
succeeds.
</p>
<div className="space-y-2">
<div className="flex gap-2 items-center">
<span className="text-sm text-kumo-subtle">Max attempts</span>
<Input
aria-label="Max retry attempts"
type="number"
value={queueAttempts}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setQueueAttempts(e.target.value)
}
className="w-20"
min={1}
max={10}
/>
</div>
<Button
variant="primary"
onClick={handleQueueRetry}
className="w-full"
>
Queue Task
</Button>
</div>
</Surface>
{/* Clear */}
<Button variant="ghost" onClick={handleClear} className="w-full">
Clear Logs
</Button>
</div>
{/* Logs */}
<div className="space-y-6">
<LogPanel logs={logs} onClear={handleClear} maxHeight="500px" />
</div>
</div>
<CodeExplanation sections={codeSections} />
</DemoWrapper>
);
}