← Vytvářejte agenty na Cloudflare / agents / runtime / communication
Připojení pouze pro čtení
Připojení pouze pro čtení brání určitým klientům WebSocket měnit stav agenta, ale zároveň jim umožňují přijímat aktualizace stavu a volat metody RPC, které stav nemění.
Přehled
Když je připojení označeno jako pouze pro čtení:
- To přijímá aktualizace stavu ze serveru
- To může volat metody RPC, které nemění stav
- To nelze volání
this.setState(): ani prostřednictvím klientské stranysetState()ani přes@callable()metodu, která voláthis.setState()interně
Toto se hodí pro scénáře jako:
- Režimy pouze pro zobrazení: Uživatelé, kteří by měli pouze sledovat, ale ne upravovat
- Přístup na základě rolí: Omezení úprav stavu na základě rolí uživatelů
- Vícetenantní scénáře: Někteří tenanti mají přístup pouze pro čtení
- Audit a sledování připojení: Pozorovatelé, kteří by neměli ovlivňovat systém
import { Agent } from "agents";
export class DocAgent extends Agent {
shouldConnectionBeReadonly(connection, ctx) {
const url = new URL(ctx.request.url);
return url.searchParams.get("mode") === "view";
}
}import { Agent, type Connection, type ConnectionContext } from "agents";
export class DocAgent extends Agent<Env, DocState> {
shouldConnectionBeReadonly(connection: Connection, ctx: ConnectionContext) {
const url = new URL(ctx.request.url);
return url.searchParams.get("mode") === "view";
}
}// Client - view-only mode
const agent = useAgent({
agent: "DocAgent",
name: "doc-123",
query: { mode: "view" },
onStateUpdateError: (error) => {
toast.error("You're in view-only mode");
},
});// Client - view-only mode
const agent = useAgent({
agent: "DocAgent",
name: "doc-123",
query: { mode: "view" },
onStateUpdateError: (error) => {
toast.error("You're in view-only mode");
},
});Označování připojení jako určených pouze pro čtení
Při připojení
Přepsat shouldConnectionBeReadonly k vyhodnocení každého připojení při jeho prvním navázání. Vraťte true k jeho označení jako jen pro čtení.
export class MyAgent extends Agent {
shouldConnectionBeReadonly(connection, ctx) {
const url = new URL(ctx.request.url);
const role = url.searchParams.get("role");
return role === "viewer" || role === "guest";
}
}export class MyAgent extends Agent<Env, State> {
shouldConnectionBeReadonly(
connection: Connection,
ctx: ConnectionContext,
): boolean {
const url = new URL(ctx.request.url);
const role = url.searchParams.get("role");
return role === "viewer" || role === "guest";
}
}Tento hook se spustí ještě před odesláním počátečního stavu klientovi, takže je připojení od úplně první zprávy pouze pro čtení.
Kdykoli
Použijte setConnectionReadonly pro dynamickou změnu stavu pouze pro čtení u připojení:
export class GameAgent extends Agent {
@callable()
async startSpectating() {
const { connection } = getCurrentAgent();
if (connection) {
this.setConnectionReadonly(connection, true);
}
}
@callable()
async joinAsPlayer() {
const { connection } = getCurrentAgent();
if (connection) {
this.setConnectionReadonly(connection, false);
}
}
}export class GameAgent extends Agent<Env, GameState> {
@callable()
async startSpectating() {
const { connection } = getCurrentAgent();
if (connection) {
this.setConnectionReadonly(connection, true);
}
}
@callable()
async joinAsPlayer() {
const { connection } = getCurrentAgent();
if (connection) {
this.setConnectionReadonly(connection, false);
}
}
}Umožnění připojení přepínat vlastní stav
Připojení může přepínat svůj vlastní stav jen pro čtení pomocí volatelné funkce. To se hodí pro rozhraní typu zamknout/odemknout, kde si diváci mohou zvolit režim úprav:
import { Agent, callable, getCurrentAgent } from "agents";
export class CollabAgent extends Agent {
@callable()
async setMyReadonly(readonly) {
const { connection } = getCurrentAgent();
if (connection) {
this.setConnectionReadonly(connection, readonly);
}
}
}import { Agent, callable, getCurrentAgent } from "agents";
export class CollabAgent extends Agent<Env, State> {
@callable()
async setMyReadonly(readonly: boolean) {
const { connection } = getCurrentAgent();
if (connection) {
this.setConnectionReadonly(connection, readonly);
}
}
}Na klientovi:
// Toggle between readonly and writable
await agent.call("setMyReadonly", [true]); // lock
await agent.call("setMyReadonly", [false]); // unlock// Toggle between readonly and writable
await agent.call("setMyReadonly", [true]); // lock
await agent.call("setMyReadonly", [false]); // unlockKontrola stavu
Použijte isConnectionReadonly pro kontrolu aktuálního stavu připojení:
export class MyAgent extends Agent {
@callable()
async getPermissions() {
const { connection } = getCurrentAgent();
if (connection) {
return { canEdit: !this.isConnectionReadonly(connection) };
}
}
}export class MyAgent extends Agent<Env, State> {
@callable()
async getPermissions() {
const { connection } = getCurrentAgent();
if (connection) {
return { canEdit: !this.isConnectionReadonly(connection) };
}
}
}Zpracování chyb na klientovi
Chyby se projevují dvěma způsoby v závislosti na tom, jak byl zápis proveden:
- Na straně klienta
setState(): server odešlecf_agent_state_errorzprávu. Zpracujte ji pomocíonStateUpdateErrorcallback. @callable()metody : volání RPC skončí chybou. Ošetřete ji pomocítry/catchkolemagent.call().
const agent = useAgent({
agent: "MyAgent",
name: "instance",
// Fires when client-side setState() is blocked
onStateUpdateError: (error) => {
setError(error);
},
});
// Fires when a callable that writes state is blocked
try {
await agent.call("updateSettings", [newSettings]);
} catch (e) {
setError(e instanceof Error ? e.message : String(e)); // "Connection is readonly"
}const agent = useAgent({
agent: "MyAgent",
name: "instance",
// Fires when client-side setState() is blocked
onStateUpdateError: (error) => {
setError(error);
},
});
// Fires when a callable that writes state is blocked
try {
await agent.call("updateSettings", [newSettings]);
} catch (e) {
setError(e instanceof Error ? e.message : String(e)); // "Connection is readonly"
}Abyste chybám vůbec předešli, ověřte oprávnění ještě před vykreslením ovládacích prvků pro úpravy:
function Editor() {
const [canEdit, setCanEdit] = useState(false);
const agent = useAgent({ agent: "MyAgent", name: "instance" });
useEffect(() => {
agent.call("getPermissions").then((p) => setCanEdit(p.canEdit));
}, []);
return <button disabled={!canEdit}>{canEdit ? "Edit" : "View Only"}</button>;
}Referenční dokumentace API
shouldConnectionBeReadonly
Přepisovatelný hook, který určuje, zda se připojení při navázání označí jako jen pro čtení.
| Parametr | Typ | Popis |
|---|---|---|
connection |
Connection |
Připojující se klient |
ctx |
ConnectionContext |
Obsahuje požadavek na upgrade |
| Vrací | boolean |
true k označení jako jen pro čtení |
Výchozí hodnota: vrací false (všechna připojení jsou zapisovatelná).
setConnectionReadonly
Označit nebo zrušit označení připojení jako určené pouze pro čtení. Lze volat kdykoli.
| Parametr | Typ | Popis |
|---|---|---|
connection |
Connection |
Připojení k aktualizaci |
readonly |
boolean |
true k nastavení jen pro čtení (výchozí hodnota: true) |
isConnectionReadonly
Zkontroluje, zda je připojení aktuálně jen pro čtení.
| Parametr | Typ | Popis |
|---|---|---|
connection |
Connection |
Připojení ke kontrole |
| Vrací | boolean |
true pokud je jen pro čtení |
onStateUpdateError (klient)
Callback při AgentClient a useAgent možnosti. Volá se, když server odmítne aktualizaci stavu.
| Parametr | Typ | Popis |
|---|---|---|
error |
string |
Chybová zpráva ze serveru |
Příklady
Přístup založený na parametru dotazu
export class DocumentAgent extends Agent {
shouldConnectionBeReadonly(connection, ctx) {
const url = new URL(ctx.request.url);
const mode = url.searchParams.get("mode");
return mode === "view";
}
}
// Client connects with readonly mode
const agent = useAgent({
agent: "DocumentAgent",
name: "doc-123",
query: { mode: "view" },
onStateUpdateError: (error) => {
toast.error("Document is in view-only mode");
},
});export class DocumentAgent extends Agent<Env, DocumentState> {
shouldConnectionBeReadonly(
connection: Connection,
ctx: ConnectionContext,
): boolean {
const url = new URL(ctx.request.url);
const mode = url.searchParams.get("mode");
return mode === "view";
}
}
// Client connects with readonly mode
const agent = useAgent({
agent: "DocumentAgent",
name: "doc-123",
query: { mode: "view" },
onStateUpdateError: (error) => {
toast.error("Document is in view-only mode");
},
});Řízení přístupu na základě rolí
export class CollaborativeAgent extends Agent {
shouldConnectionBeReadonly(connection, ctx) {
const url = new URL(ctx.request.url);
const role = url.searchParams.get("role");
return role === "viewer" || role === "guest";
}
onConnect(connection, ctx) {
const url = new URL(ctx.request.url);
const userId = url.searchParams.get("userId");
console.log(
`User ${userId} connected (readonly: ${this.isConnectionReadonly(connection)})`,
);
}
@callable()
async upgradeToEditor() {
const { connection } = getCurrentAgent();
if (!connection) return;
// Check permissions (pseudo-code)
const canUpgrade = await checkUserPermissions();
if (canUpgrade) {
this.setConnectionReadonly(connection, false);
return { success: true };
}
throw new Error("Insufficient permissions");
}
}export class CollaborativeAgent extends Agent<Env, CollabState> {
shouldConnectionBeReadonly(
connection: Connection,
ctx: ConnectionContext,
): boolean {
const url = new URL(ctx.request.url);
const role = url.searchParams.get("role");
return role === "viewer" || role === "guest";
}
onConnect(connection: Connection, ctx: ConnectionContext) {
const url = new URL(ctx.request.url);
const userId = url.searchParams.get("userId");
console.log(
`User ${userId} connected (readonly: ${this.isConnectionReadonly(connection)})`,
);
}
@callable()
async upgradeToEditor() {
const { connection } = getCurrentAgent();
if (!connection) return;
// Check permissions (pseudo-code)
const canUpgrade = await checkUserPermissions();
if (canUpgrade) {
this.setConnectionReadonly(connection, false);
return { success: true };
}
throw new Error("Insufficient permissions");
}
}Administrátorský dashboard
export class MonitoringAgent extends Agent {
shouldConnectionBeReadonly(connection, ctx) {
const url = new URL(ctx.request.url);
// Only admins can modify state
return url.searchParams.get("admin") !== "true";
}
onStateChanged(state, source) {
if (source !== "server") {
// Log who modified the state
console.log(`State modified by connection ${source.id}`);
}
}
}
// Admin client (can modify)
const adminAgent = useAgent({
agent: "MonitoringAgent",
name: "system",
query: { admin: "true" },
});
// Viewer client (readonly)
const viewerAgent = useAgent({
agent: "MonitoringAgent",
name: "system",
query: { admin: "false" },
onStateUpdateError: (error) => {
console.log("Viewer cannot modify state");
},
});export class MonitoringAgent extends Agent<Env, SystemState> {
shouldConnectionBeReadonly(
connection: Connection,
ctx: ConnectionContext,
): boolean {
const url = new URL(ctx.request.url);
// Only admins can modify state
return url.searchParams.get("admin") !== "true";
}
onStateChanged(state: SystemState, source: Connection | "server") {
if (source !== "server") {
// Log who modified the state
console.log(`State modified by connection ${source.id}`);
}
}
}
// Admin client (can modify)
const adminAgent = useAgent({
agent: "MonitoringAgent",
name: "system",
query: { admin: "true" },
});
// Viewer client (readonly)
const viewerAgent = useAgent({
agent: "MonitoringAgent",
name: "system",
query: { admin: "false" },
onStateUpdateError: (error) => {
console.log("Viewer cannot modify state");
},
});Dynamické změny oprávnění
export class GameAgent extends Agent {
@callable()
async startSpectatorMode() {
const { connection } = getCurrentAgent();
if (!connection) return;
this.setConnectionReadonly(connection, true);
return { mode: "spectator" };
}
@callable()
async joinAsPlayer() {
const { connection } = getCurrentAgent();
if (!connection) return;
const canJoin = this.state.players.length < 4;
if (canJoin) {
this.setConnectionReadonly(connection, false);
return { mode: "player" };
}
throw new Error("Game is full");
}
@callable()
async getMyPermissions() {
const { connection } = getCurrentAgent();
if (!connection) return null;
return {
canEdit: !this.isConnectionReadonly(connection),
connectionId: connection.id,
};
}
}export class GameAgent extends Agent<Env, GameState> {
@callable()
async startSpectatorMode() {
const { connection } = getCurrentAgent();
if (!connection) return;
this.setConnectionReadonly(connection, true);
return { mode: "spectator" };
}
@callable()
async joinAsPlayer() {
const { connection } = getCurrentAgent();
if (!connection) return;
const canJoin = this.state.players.length < 4;
if (canJoin) {
this.setConnectionReadonly(connection, false);
return { mode: "player" };
}
throw new Error("Game is full");
}
@callable()
async getMyPermissions() {
const { connection } = getCurrentAgent();
if (!connection) return null;
return {
canEdit: !this.isConnectionReadonly(connection),
connectionId: connection.id,
};
}
}Komponenta React na straně klienta:
function GameComponent() {
const [canEdit, setCanEdit] = useState(false);
const agent = useAgent({
agent: "GameAgent",
name: "game-123",
onStateUpdateError: (error) => {
toast.error("Cannot modify game state in spectator mode");
},
});
useEffect(() => {
agent.call("getMyPermissions").then((perms) => {
setCanEdit(perms?.canEdit ?? false);
});
}, [agent]);
return (
<div>
<button onClick={() => agent.call("joinAsPlayer")} disabled={canEdit}>
Join as Player
</button>
<button
onClick={() => agent.call("startSpectatorMode")}
disabled={!canEdit}
>
Switch to Spectator
</button>
<div>{canEdit ? "You can modify the game" : "You are spectating"}</div>
</div>
);
}Jak to funguje
Stav pouze pro čtení se ukládá do přílohy WebSocketu daného připojení, která přetrvá i díky WebSocket Hibernation API. Příznak je interně rozdělený do jmenných prostorů, takže ho nelze nedopatřením přepsat pomocí connection.setState(). Stejný mechanismus používá řízení zpráv protokolu : Oba příznaky mohou v příloze bezpečně koexistovat. To znamená:
- Přežije hibernaci : příznak se serializuje a obnoví se, jakmile se agent probudí
- Není potřeba žádné čištění : Stav připojení se po jeho uzavření automaticky zahodí
- Nulová režie : žádné databázové tabulky ani dotazy, jen vestavěná příloha připojení
- Bezpečné před uživatelským kódem,
connection.stateaconnection.setState()nikdy nezveřejňujte ani nepřepisujte příznak pouze pro čtení
Když se připojení pouze pro čtení pokusí upravit stav, server to zablokuje, bez ohledu na to, zda zápis pochází ze strany klienta setState() nebo z @callable() metoda:
Client (readonly) Agent
│ │
│ setState({ count: 1 }) │
│ ─────────────────────────────▶ │ Check readonly → blocked
│ ◀─────────────────────────── │
│ cf_agent_state_error │
│ │
│ call("increment") │
│ ─────────────────────────────▶ │ increment() calls this.setState()
│ │ Check readonly → throw
│ ◀─────────────────────────── │
│ RPC error: "Connection is │
│ readonly" │
│ │
│ call("getPermissions") │
│ ─────────────────────────────▶ │ getPermissions() — no setState()
│ ◀─────────────────────────── │
│ RPC result: { canEdit: false }│Co readonly omezuje a co ne
| Akce | Povoleno? |
|---|---|
| Přijímejte vysílání stavu | Ano |
Zavolejte @callable() metody, které nezapisují stav |
Ano |
Zavolejte @callable() metody, které volají this.setState() |
Ne |
Odesílejte aktualizace stavu prostřednictvím setState() |
Ne |
Vynucení probíhá uvnitř setState() samotné. Když @callable() metoda se pokusí zavolat this.setState() a aktuální kontext připojení je jen pro čtení, framework vyvolá Error("Connection is readonly"). To znamená, že ve svých metodách RPC nepotřebujete ruční kontroly oprávnění: každý callable, který zapisuje stav, se u připojení jen pro čtení automaticky zablokuje.
Omezení
Vedlejší efekty ve volatelných funkcích se přesto provedou
Kontrola readonly probíhá uvnitř this.setState(), nikoli na začátku volatelné metody. Pokud má vaše metoda vedlejší efekty ještě před zápisem stavu, tyto se přesto provedou:
export class MyAgent extends Agent {
@callable()
async processOrder(orderId) {
await sendConfirmationEmail(orderId); // runs even for readonly connections
await chargePayment(orderId); // runs too
this.setState({ ...this.state, orders: [...this.state.orders, orderId] }); // throws
}
}export class MyAgent extends Agent<Env, State> {
@callable()
async processOrder(orderId: string) {
await sendConfirmationEmail(orderId); // runs even for readonly connections
await chargePayment(orderId); // runs too
this.setState({ ...this.state, orders: [...this.state.orders, orderId] }); // throws
}
}Abyste se tomu vyhnuli, buď ověřte oprávnění před vedlejšími efekty, nebo kód uspořádejte tak, aby zápis stavu proběhl jako první:
export class MyAgent extends Agent {
@callable()
async processOrder(orderId) {
// Write state first — throws immediately for readonly connections
this.setState({ ...this.state, orders: [...this.state.orders, orderId] });
// Side effects only run if setState succeeded
await sendConfirmationEmail(orderId);
await chargePayment(orderId);
}
}export class MyAgent extends Agent<Env, State> {
@callable()
async processOrder(orderId: string) {
// Write state first — throws immediately for readonly connections
this.setState({ ...this.state, orders: [...this.state.orders, orderId] });
// Side effects only run if setState succeeded
await sendConfirmationEmail(orderId);
await chargePayment(orderId);
}
}Osvědčené postupy
Kombinace s autentizací
export class SecureAgent extends Agent {
shouldConnectionBeReadonly(connection, ctx) {
const url = new URL(ctx.request.url);
const token = url.searchParams.get("token");
// Verify token and get permissions
const permissions = this.verifyToken(token);
return !permissions.canWrite;
}
}export class SecureAgent extends Agent<Env, State> {
shouldConnectionBeReadonly(
connection: Connection,
ctx: ConnectionContext,
): boolean {
const url = new URL(ctx.request.url);
const token = url.searchParams.get("token");
// Verify token and get permissions
const permissions = this.verifyToken(token);
return !permissions.canWrite;
}
}Poskytování jasné zpětné vazby uživatelům
const agent = useAgent({
agent: "MyAgent",
name: "instance",
onStateUpdateError: (error) => {
// User-friendly messages
if (error.includes("readonly")) {
showToast("You are in view-only mode. Upgrade to edit.");
}
},
});const agent = useAgent({
agent: "MyAgent",
name: "instance",
onStateUpdateError: (error) => {
// User-friendly messages
if (error.includes("readonly")) {
showToast("You are in view-only mode. Upgrade to edit.");
}
},
});Zkontrolujte oprávnění před akcemi UI
function EditButton() {
const [canEdit, setCanEdit] = useState(false);
const agent = useAgent({
/* ... */
});
useEffect(() => {
agent.call("checkPermissions").then((perms) => {
setCanEdit(perms.canEdit);
});
}, []);
return <button disabled={!canEdit}>{canEdit ? "Edit" : "View Only"}</button>;
}Protokolujte pokusy o přístup
export class AuditedAgent extends Agent {
onStateChanged(state, source) {
if (source !== "server") {
this.audit({
action: "state_update",
connectionId: source.id,
readonly: this.isConnectionReadonly(source),
timestamp: Date.now(),
});
}
}
}export class AuditedAgent extends Agent<Env, State> {
onStateChanged(state: State, source: Connection | "server") {
if (source !== "server") {
this.audit({
action: "state_update",
connectionId: source.id,
readonly: this.isConnectionReadonly(source),
timestamp: Date.now(),
});
}
}
}Omezení
- Stav pouze pro čtení se vztahuje pouze na aktualizace stavu pomocí
setState() - Metody RPC lze i nadále volat (v případě potřeby si implementujte vlastní kontroly)
- Příznak pouze pro čtení je vlastností jednotlivého připojení, nikoli identity uživatele
Související zdroje
- Ukládání a synchronizace stavu
- Zprávy protokolu : potlačí rámce protokolu JSON pro klienty, kteří pracují pouze s binárními daty (lze kombinovat s readonly)
- WebSockets
- Volatelné metody