mirror of
https://github.com/nestriness/nestri.git
synced 2026-11-03 14:12:39 +02:00
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:
@@ -92,6 +92,27 @@ export namespace SessionApi {
|
||||
.strict();
|
||||
|
||||
export const route = new Hono()
|
||||
.get(
|
||||
'/',
|
||||
notPublic,
|
||||
describeRoute({
|
||||
tags: ['Session'],
|
||||
summary: 'Your runs that have not stopped',
|
||||
description:
|
||||
'At most one per box, newest first. Read this when asking for a run is refused because a box is busy: the run holding it may be the same game, which a client can rejoin by polling it, or another one it can offer to stop.',
|
||||
responses: {
|
||||
200: {
|
||||
content: { 'application/json': { schema: Result(Session.Info.array()) } },
|
||||
description: 'The runs still going, or still asked for'
|
||||
},
|
||||
401: ErrorResponses[401],
|
||||
403: ErrorResponses[403]
|
||||
}
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json({ data: await Session.activeForOwner(actingPerson()) });
|
||||
}
|
||||
)
|
||||
.post(
|
||||
'/',
|
||||
notPublic,
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user