feat(session): list your runs that have not stopped

Asking for a run on a busy box is refused, but the refusal does not say
what holds the box. A client that just lost its connection needs to know
whether the run in the way is the game it asked for, which it can rejoin,
or another one it should offer to stop. GET /session answers that.
This commit is contained in:
Wanjohi
2026-10-10 11:19:20 +03:00
parent 152337296d
commit 05a3fee87a
3 changed files with 75 additions and 0 deletions
+27
View File
@@ -513,6 +513,33 @@ export namespace Session {
});
});
/**
* The runs of someone's boxes that have not stopped, newest first.
*
* At most one per box, by the same index {@link activeForBox} leans on. A
* client reads this to tell "your box is busy" apart from "your box is
* busy with the game you just asked for", which it can rejoin rather than
* stop.
*/
export const activeForOwner = fn(z.string(), async (userId) => {
return Database.use(async (tx) => {
return tx
.select({ session: SessionTable })
.from(SessionTable)
.innerJoin(BoxTable, eq(SessionTable.boxId, BoxTable.id))
.where(
and(
eq(BoxTable.userId, userId),
isNull(SessionTable.timeDeleted),
isNull(SessionTable.timeStopped),
isNull(BoxTable.timeDeleted)
)
)
.orderBy(desc(SessionTable.timeCreated))
.then((rows) => rows.map((row) => serialize(row.session)));
});
});
/**
* The owner asks for a run to end.
*
+27
View File
@@ -186,6 +186,33 @@ describe('Session', () => {
expect((await Session.activeForBox(box.id))?.id).toBe(second.id);
});
test('an owner sees their own runs that have not stopped, and nobody else’s', async () => {
const mine = await scene('ses-owner-a', 5406);
const theirs = await scene('ses-owner-b', 5407);
const ended = await Session.create({
id: Identifier.ascending('session'),
boxId: mine.box.id,
gameId: mine.gameId,
linkedAccountId: mine.owner.linkedAccountId
});
await Session.setState({ id: ended.id, state: 'ended', errorMessage: null });
const going = await Session.create({
id: Identifier.ascending('session'),
boxId: mine.box.id,
gameId: mine.gameId,
linkedAccountId: mine.owner.linkedAccountId
});
await Session.create({
id: Identifier.ascending('session'),
boxId: theirs.box.id,
gameId: theirs.gameId,
linkedAccountId: theirs.owner.linkedAccountId
});
const active = await Session.activeForOwner(mine.owner.userId);
expect(active.map((s) => s.id)).toEqual([going.id]);
});
test('deleting a box takes its sessions with it', async () => {
const { owner, box, gameId } = await scene('ses-cascade', 5405);
await Session.create({