Managing Global User State
Every player has a persistent state document of their own — profile, preferences, progression. It contains exactly what you have written to it and survives across sessions.
User state is not seeded with an account field. A user who has never been
written to returns {}, so use $sender.account for the caller’s identity
rather than reading it out of the state. The batch reader
$global.getUserStates(accounts) is the exception — it adds account to
each entry so you can tell the rows apart.
Server API
$global.getMyState(paths?: string[]): Promise<Object>: The caller’s state. Equivalent to$global.getUserState($sender.account, paths).$global.updateMyState(state: Object, opts?: { returnState?: boolean }): Promise<Object | null>: Equivalent to$global.updateUserState($sender.account, state, opts).$global.getUserState(account: string, paths?: string[]): Promise<Object>: Any user’s state.pathsworks as in Partial Fetch.$global.getUserStates(accounts: string[], paths?: string[]): Promise<Array<Object & { account: string }>>: Many users in one call — see Batch Read.$global.updateUserState(account: string, state: Object, opts?: { returnState?: boolean }): Promise<Object | null>: Shallow-merges into a user’s state; a top-levelundefineddeletes that field. Resolves with the patch you passed — see Fire-and-forget writes.$global.clearUserState(account: string, fields?: string[]): Promise<Object>/$global.clearMyState(fields?: string[]): Promise<Object>: Delete a user’s state. No argument wipes every user-defined field;clear([ids])deletes only those top-level fields (not paths);clear([])is a no-op. Persists immediately.
User state has the same persistence limits as global state.
export class Server {
// $sender.account is filled in by the server from the caller's connection,
// so a write routed through updateMyState can only touch the caller.
async setNickname(nickname) {
if (typeof nickname !== "string" || nickname.trim().length === 0) {
throw new Error("Invalid nickname");
}
const clean = nickname.trim().slice(0, 16);
await $global.updateMyState({ nickname: clean });
return { nickname: clean };
}
}Never take the target of updateUserState(account, …) from the client. Every Server method
is a public endpoint, so a method that hands
its account argument straight to updateUserState lets one player rewrite another player’s
level or inventory. Write the caller’s own state through updateMyState, and let a write that
targets someone else happen only as the outcome of a rule the server checks. To move currency
between players, use $asset.transfer.
Batch Read
getUserStates(accounts, paths?) reads many accounts in one call instead of one call per account — use it for leaderboards, party/guild rosters, or per-player progression inside a tick.
export class Server {
async getPartyStatus(accounts) {
// Combine with paths to keep the payload small.
const states = await $global.getUserStates(accounts, ['profile.name', 'stats.hp']);
// → [{ account: '0xa…', profile: { name: 'alice' }, stats: { hp: 100 } }, …]
return states;
}
}- Order-preserving and 1:1:
result[i].account === accounts[i]. - An account with no state comes back as
{ account }only — useObject.hasOwn(result[i], 'profile')to tell “absent” from “empty”. paths === []returns{ account }per entry with no body read; omittingpathsreturns each account’s full state.accounts === []returns[]. There is no hard cap on list length — paginate yourself and passpathson hot paths.- The snapshot is best-effort: it is not atomic across accounts, so entries may come from slightly different instants.
Client API
server.subscribeGlobalMyState((state: any) => {}): UnsubscribeFunctionserver.subscribeGlobalUserState(account: string, (state: any) => {}): UnsubscribeFunctionconst myState = useGlobalMyState()const userState = useGlobalUserState(account: string)
The hooks subscribe and unsubscribe for you. For the raw subscribe* pattern, see Global State.
Example: Player Profile
The client renders its own profile and a friend’s, live, and renames itself through the setNickname server function above.
import { useGameServer, useGlobalMyState, useGlobalUserState } from "@agent8/gameserver";
export default function Profile({ friend }: { friend: string }) {
const { connected, server } = useGameServer();
const me = useGlobalMyState(); // re-renders when your state changes
const them = useGlobalUserState(friend); // any account — read-only on the client
if (!connected) return <p>Loading...</p>;
const rename = async () => {
const nickname = prompt("New nickname:");
if (nickname) await server.remoteFunction("setNickname", [nickname]);
};
return (
<div>
<p>You: {me.nickname ?? "(no nickname)"}</p>
<p>Friend: {them.nickname ?? friend}</p>
<button onClick={rename}>Change nickname</button>
</div>
);
}