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{}ornull. Wrap it intry/catch, or usegetCollectionItemswith a filter when a miss is an expected outcome. -
$room.countCollectionItems(collectionId: string, options?: CollectionOptions): Promise<number>
Returns the number of items matchingfilters. 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 bylimitif you pass one). Treat a result of exactly 1000 — or of yourlimit— 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. Passoptions.idto 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. Withoutoptions.id, an id is generated automatically. -
$room.updateCollectionItem(collectionId: string, item: any): Promise<{ __id: string }>
Mergesiteminto an existing room-collection item. The item you pass must carry__id— it is how the target is found, and the call throwsItem 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 withgetCollectionItemif 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[]} }) => {}): UnsubscribeFunctionconst { 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.
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" }],
});
}
}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>
);
}