update shape of room instance browser

This commit is contained in:
Devin Zuczek
2026-08-04 17:19:03 -04:00
parent 73bb7c4609
commit 7d300fa836
5 changed files with 128 additions and 8 deletions
+29
View File
@@ -174,6 +174,35 @@ export async function countPlayersByRoom(
return new Map(results.map((r) => [r.roomId, r.n]))
}
/**
* Who is standing in each of a room's instances right now, keyed by instance id —
* one grouped query rather than a lookup per instance, so the owner's instance list
* stays a single read. Reads only unexpired presence; instances nobody is in are
* simply absent from the map (callers default to an empty list), and lobby
* (null-instance) presence is excluded.
*/
export async function getPlayerIdsByRoomInstance(
db: D1Database,
roomId: number,
now = nowSeconds()
): Promise<Map<number, number[]>> {
const { results } = await db
.prepare(
`SELECT room_instance_id AS instanceId, account_id AS accountId FROM presence
WHERE room_id = ?1 AND expires_at > ?2 AND room_instance_id IS NOT NULL
ORDER BY account_id`
)
.bind(roomId, now)
.all<{ instanceId: number; accountId: number }>()
const out = new Map<number, number[]>()
for (const r of results) {
const players = out.get(r.instanceId)
if (players) players.push(r.accountId)
else out.set(r.instanceId, [r.accountId])
}
return out
}
/**
* The room instances that expired presence rows still point at — the instances a
* player was in when they stopped heartbeating (a crash or a hard quit, where no
+48 -1
View File
@@ -12,7 +12,7 @@
* the client DTO (`toDto`).
*/
import { countPlayersInInstance } from './presence-db'
import { countPlayersInInstance, getPlayerIdsByRoomInstance } from './presence-db'
/** Schema DDL (mirror of migrations/0004_room_instance.sql). */
export const ROOM_INSTANCE_SCHEMA_DDL: string[] = [
@@ -69,6 +69,22 @@ export interface RoomInstanceDto {
createdAt: string
}
/**
* The owner's view of one live instance of their room (`match`:
* `GET /room/:roomId/instances`). Deliberately NOT the client `RoomInstanceDto`:
* it's a management listing, so it carries who is in there (`playerIds`, from live
* presence) and drops the connection details (photon ids, data blob, room code) an
* owner has no business reading for a session they aren't in.
*/
export interface RoomInstanceSummary {
roomInstanceId: number
roomId: number
subRoomId: number
isFull: boolean
createdAt: string
playerIds: number[]
}
/** The full stored instance — the DTO plus the JsonIgnore fields (in the blob). */
interface StoredRoomInstance extends RoomInstanceDto {
ownerAccountId: number
@@ -292,3 +308,34 @@ export async function getRoomInstancesByRoom(
.all<{ data: string }>()
return results.map((r) => toDto(parse(r.data)))
}
/**
* A room's instances as the owner's management listing sees them — the
* {@link RoomInstanceSummary} projection, each with the players currently standing
* in it. Presence is read once for the whole room (one grouped query), so this stays
* two reads regardless of how many instances are live; an instance nobody is in
* (everyone timed out, or it was just created) gets an empty `playerIds`.
*/
export async function getRoomInstanceSummariesByRoom(
db: D1Database,
roomId: number
): Promise<RoomInstanceSummary[]> {
const [{ results }, playersByInstance] = await Promise.all([
db
.prepare('SELECT data FROM room_instance WHERE room_id = ?1 ORDER BY id')
.bind(roomId)
.all<{ data: string }>(),
getPlayerIdsByRoomInstance(db, roomId),
])
return results.map((r) => {
const s = parse(r.data)
return {
roomInstanceId: s.roomInstanceId,
roomId: s.roomId,
subRoomId: s.subRoomId,
isFull: s.isFull,
createdAt: s.createdAt,
playerIds: playersByInstance.get(s.roomInstanceId) ?? [],
}
})
}