Skip to Content
Game ServerRoom Collection

Managing Room Collection Data

Room collections have the same API as Global Collection — called on $room instead of $global — but each room has its own. Filters, sorting and pagination work the same way; the differences are noted below.

⚠️

Lifetime: a room collection lives as long as its room. When the last player leaves it is deleted right after onRoomDestroy runs, so the hook can still read it — copy anything worth keeping into a global collection there. It is never written to durable storage, so it is also lost if the room’s server goes away.

Server API

  • $room.getCollectionItems(collectionId: string, options?: CollectionOptions): Promise<Item[]>
    Retrieves multiple items from a room collection based on filtering, sorting, and pagination options.

  • $room.getCollectionItem(collectionId: string, itemId: string): Promise<Item>
    Retrieves a single item from the room collection using its unique ID. It throws when the item does not exist (Item <id> not found in room <roomId> collection <cid>) — it does not return {} or null. Wrap it in try/catch, or use getCollectionItems with a filter when a miss is an expected outcome.

  • $room.countCollectionItems(collectionId: string, options?: CollectionOptions): Promise<number>
    Returns the number of items matching filters. It can undercount. Only a query the index can answer entirely is counted exactly; anything else falls back to materialising the matches and taking their length, which is capped at 1000 (and further clamped by limit if you pass one). Treat a result of exactly 1000 — or of your limit — as “at least that many”.

  • $room.addCollectionItem(collectionId: string, item: any, options?: { id?: string }): Promise<Item>
    Adds a new item to the specified room collection and returns the added item with its unique identifier. Pass options.id to control the item id (upsert / full overwrite). The id must be a non-empty string with no /, not ./.., not matching __...__, and ≤1500 bytes; an invalid id throws. Without options.id, an id is generated automatically.

  • $room.updateCollectionItem(collectionId: string, item: any): Promise<{ __id: string }>
    Merges item into an existing room-collection item. The item you pass must carry __id — it is how the target is found, and the call throws Item must have __id to update… without it. It also throws if no item with that id exists. The return value is only { __id }, not the merged item; read it back with getCollectionItem if you need the result.

  • $room.deleteCollection(collectionId: string): Promise<{ collectionId: string }>
    Deletes the entire room collection. Destructive and not undoable — there is no durable copy to restore from. Use it to reset a collection between matches in a room that stays alive.

  • $room.deleteCollectionItem(collectionId: string, itemId: string): Promise<{ __id: string }>
    Deletes an item from the room collection and returns a confirmation containing the deleted item’s ID.

🚫

Do not re-export these methods as remote functions under their own names. Every Server method is a public endpoint, so a pass-through addCollectionItem(collectionId, item) lets anyone write { score: 999999 } into scores. Let server logic decide the collection and the values — the Room Scoreboard below takes the player from $sender.account.

Client API

  • server.subscribeRoomCollection(roomId, collectionId: string, ({ items, changes }: { items: any[], changes: { op: 'add'|'update'|'delete'|'deleteAll', items: any[]} }) => {}): UnsubscribeFunction
  • const { items } = useRoomCollection(collectionId) — the current room; subscribes for you.

Both need the Room Server connection (rsConnected).

Example: Room Scoreboard

A per-room scoreboard. The server builds each item itself; the client renders the live list from useRoomCollection.

server/src/server.ts
export class Server { async addScore(points) { const myState = await $room.getMyState(); const scoreItem = { playerName: myState.name || "Anonymous", playerAccount: $sender.account, score: points, timestamp: Date.now(), }; return await $room.addCollectionItem("scores", scoreItem); } async getTopScores(limit = 10) { return await $room.getCollectionItems("scores", { limit, orderBy: [{ field: "score", direction: "desc" }], }); } }
src/Scoreboard.tsx
import { useGameServer, useRoomCollection } from "@agent8/gameserver"; import { useState } from "react"; export default function Scoreboard() { // useRoomCollection only yields data once the Room Server connection is up. const { server, rsConnected } = useGameServer(); const { items: scores } = useRoomCollection("scores"); const [newScore, setNewScore] = useState(0); if (!rsConnected) return <p>Joining room...</p>; const handleAddScore = async () => { if (newScore > 0) { await server.remoteFunction("addScore", [newScore]); setNewScore(0); } }; // Sort scores by highest first const sortedScores = [...scores].sort((a, b) => b.score - a.score); return ( <div> <h2>Room Scoreboard</h2> <div> <input type="number" value={newScore} onChange={(e) => setNewScore(parseInt(e.target.value) || 0)} min="0" /> <button onClick={handleAddScore}>Add Score</button> </div> <table style={{ width: "100%", marginTop: "20px" }}> <thead> <tr> <th>Rank</th> <th>Player</th> <th>Score</th> <th>Date</th> </tr> </thead> <tbody> {sortedScores.map((score, index) => ( <tr key={score.__id}> <td>{index + 1}</td> <td>{score.playerName}</td> <td>{score.score}</td> <td>{new Date(score.timestamp).toLocaleString()}</td> </tr> ))} </tbody> </table> </div> ); }
Last updated on