Overview
When a connection is marked as readonly:- It receives state updates from the server
- It can call RPC methods that do not modify state
- It cannot call
this.setState()— neither via client-sidesetState()nor via a@callable()method that callsthis.setState()internally
Marking connections as readonly
On connect
OverrideshouldConnectionBeReadonly to evaluate each connection when it first connects. Return true to mark it readonly.
At any time
UsesetConnectionReadonly to change a connection’s readonly status dynamically:
Letting a connection toggle its own status
A connection can toggle its own readonly status via a callable. This is useful for “lock/unlock” UIs where viewers can opt into editing mode:Checking status
UseisConnectionReadonly to check a connection’s current status:
Handling errors on the client
Errors surface in two ways depending on how the write was attempted:- Client-side
setState()— the server sends acf_agent_state_errormessage. Handle it with theonStateUpdateErrorcallback. @callable()methods — the RPC call rejects with an error. Handle it with atry/catcharoundagent.call().
onStateUpdateError also fires when validateStateChange rejects a client-originated state update (with the message "State update rejected"). This makes the callback useful for handling any rejected state write, not just readonly errors.API reference
shouldConnectionBeReadonly(connection, ctx)
Called when a connection is established. Override to control which connections are readonly.
Default: returns
false (all connections are writable).
setConnectionReadonly(connection, readonly?)
Mark or unmark a connection as readonly. Can be called at any time.
isConnectionReadonly(connection)
Check if a connection is currently readonly.
onStateUpdateError (client)
Callback on AgentClient and useAgent options. Called when the server rejects a state update.
How it works
Readonly status is stored in the connection’s WebSocket attachment, which persists through the WebSocket Hibernation API. The flag is namespaced internally so it cannot be accidentally overwritten byconnection.setState(). This means:
- Survives hibernation — the flag is serialized and restored when the agent wakes up
- No cleanup needed — connection state is automatically discarded when the connection closes
- Zero overhead — no database tables or queries, just the connection’s built-in attachment
- Safe from user code —
connection.stateandconnection.setState()never expose or overwrite the readonly flag
setState() or from a @callable() method:
What readonly does and does not restrict
The enforcement happens inside
setState() itself. When a @callable() method tries to call this.setState() and the current connection context is readonly, the framework throws an Error("Connection is readonly"). This means you do not need manual permission checks in your RPC methods — any callable that writes state is automatically blocked for readonly connections.
Caveats
Side effects in callables still run
The readonly check happens insidethis.setState(), not at the start of the callable. If your method has side effects before the state write, those will still execute: