Skip to Content
Game ServerGlobal User State

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. paths works 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-level undefined deletes 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.

server/src/server.ts
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.

server/src/server.ts
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 — use Object.hasOwn(result[i], 'profile') to tell “absent” from “empty”.
  • paths === [] returns { account } per entry with no body read; omitting paths returns each account’s full state.
  • accounts === [] returns []. There is no hard cap on list length — paginate yourself and pass paths on 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) => {}): UnsubscribeFunction
  • server.subscribeGlobalUserState(account: string, (state: any) => {}): UnsubscribeFunction
  • const 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.

src/Profile.tsx
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> ); }
Last updated on