INTEGRITY Dokumentace

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í:

Toto se hodí pro scénáře jako:

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]); // unlock

Kontrola 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:

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á:

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í