The host has a hand on the other seats: once the duel begins, Seats makes a transfer phrase for another wizard's seat, which gives it a new token when claimed, and gives the seat of a wizard who cannot go on to a bot, which duels the turn it owes at once and keeps gestures already sealed; the wizard's token opens nothing while it stands in, their page goes to the gallery with the reason and a claim box, the roster says a bot duels for them, and a rematch holds their seat without the stand-in; the server pings every socket and closes one a dead browser left open
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
1 parent
a673ea9766
commit
f98bfba107
14 files changed
+438
-31
No files matched your search
@@ -14,6 +14,8 @@
|
||||
// POST /api/rooms/:id/challenge {token} call the keeper of the site to a held seat; their phone rings
|
||||
// POST /api/rooms/:id/rematch {token} a finished table opens another with the same company
|
||||
// POST /api/rooms/:id/transfer {token} a four-word phrase that carries this seat to another device
|
||||
// POST /api/rooms/:id/phrase {token, seat} the host mints one for another wizard's seat, which re-seats it when claimed
|
||||
// POST /api/rooms/:id/standin {token, seat} the host gives a wizard's seat to a bot for the rest of the duel
|
||||
// POST /api/transfer {phrase} claim a phrase: the seat and its token, once
|
||||
// WS /ws?room=:id&token= {type:"update", seq} whenever the room changes; without a token,
|
||||
// a seat in the Peanut Gallery, counted for the table
|
||||
@@ -79,6 +81,8 @@ const bells = new RateLimit(3, 60 * 60 * 1000);
|
||||
/** Claims on transfer phrases: a phrase is guessed, not brute-forced. */
|
||||
const claims = new RateLimit(10, 10 * 60 * 1000);
|
||||
const MAX_AUDIENCE = 30;
|
||||
/** How often each socket is asked to answer; one silent through a whole round is a browser gone, and is closed. */
|
||||
const PING_MS = Number(process.env.PING_MS ?? 30_000);
|
||||
const IDLE_ROOM_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
setInterval(() => {
|
||||
const n = rooms.evictIdle(IDLE_ROOM_MS);
|
||||
@@ -339,6 +343,14 @@ async function handle(req: IncomingMessage, res: ServerResponse): Promise<void>
|
||||
if (action === 'transfer') {
|
||||
return send(res, 200, rooms.transfer(room, typeof body.token === 'string' ? body.token : undefined));
|
||||
}
|
||||
if (action === 'phrase') {
|
||||
return send(res, 200, rooms.hostPhrase(room, typeof body.token === 'string' ? body.token : undefined, body.seat));
|
||||
}
|
||||
if (action === 'standin') {
|
||||
const token = typeof body.token === 'string' ? body.token : undefined;
|
||||
rooms.standIn(room, token, body.seat);
|
||||
return send(res, 200, rooms.view(room, rooms.seatOf(room, token).id));
|
||||
}
|
||||
throw new RoomError('Not here.', 404);
|
||||
}
|
||||
|
||||
@@ -372,12 +384,31 @@ server.on('upgrade', (req, socket, head) => {
|
||||
};
|
||||
const unsubscribe = seatId === SPECTATOR ? rooms.watch(room, onChange) : rooms.subscribe(room.id, onChange);
|
||||
ws.on('close', unsubscribe);
|
||||
answered.set(ws, true);
|
||||
ws.on('pong', () => answered.set(ws, true));
|
||||
// A socket's own error (a message over the limit, a malformed frame) closes that socket, never the server.
|
||||
ws.on('error', () => ws.terminate());
|
||||
ws.send(JSON.stringify({ type: 'hello', seat: seatId }));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* A browser that died without closing (a crashed tab, a sleeping laptop, a dropped network) leaves its socket
|
||||
* open until TCP gives up, counted in the gallery all the while. The protocol's own ping, which every browser
|
||||
* answers by itself, finds it within two rounds.
|
||||
*/
|
||||
const answered = new WeakMap<WebSocket, boolean>();
|
||||
setInterval(() => {
|
||||
for (const ws of wss.clients) {
|
||||
if (!answered.get(ws)) {
|
||||
ws.terminate();
|
||||
continue;
|
||||
}
|
||||
answered.set(ws, false);
|
||||
ws.ping();
|
||||
}
|
||||
}, PING_MS).unref();
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
console.log(`waving-hands server listening on http://${HOST}:${PORT}, rooms in ${DATA_DIR}`);
|
||||
});
|
||||
Reference in new issue
Block a user