The template banks a browser's seats in a cookie the server sets, and gives a lost seat back by name to the address it was last used from
A seat lived in the browser's storage alone, and Safari clears that after a week away, or at a word from the person on another device. Now every seat taken is banked as well in an HttpOnly cookie the server writes, which such clearing leaves alone, and the hall takes its seats back from the bank on arrival. For a browser that lost even the cookie, the ledger notes the address each seat was last used from, as a salted hash, and a name given in the hall or at the table gets its seats back from the same address, with a new token; where the address differs, or two people at the table share it, the table is named so the host can mint a phrase. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CCLa2PaEeCj8njQZSWGqHx
This commit is contained in:
1 parent
6a9dc5a496
commit
dcb18b2b7e
7 files changed
+318
-8
No files matched your search
@@ -30,8 +30,13 @@ export interface Seat {
|
||||
bot: boolean;
|
||||
/** The host gave this person's seat to a bot; a phrase from the host, or the keeper, gives it back. */
|
||||
standIn?: true;
|
||||
/** The address this person's seat was last used from, as a salted hash; a seat their browser lost goes back to the same address. */
|
||||
addr?: string;
|
||||
}
|
||||
|
||||
/** A table whose ledger has not been written in this long is not looked through for a lost seat. */
|
||||
const LOST_SEAT_LOOKBACK_MS = 90 * 24 * 60 * 60 * 1000;
|
||||
|
||||
function hashToken(token: string): string {
|
||||
return createHash('sha256').update(token).digest('hex');
|
||||
}
|
||||
@@ -318,6 +323,51 @@ export class Rooms<State, Input> {
|
||||
return { phrase, expiresAt };
|
||||
}
|
||||
|
||||
/** The address a seat is used from, noted when it changes. Not a change the table need hear of: nothing wakes for it. */
|
||||
seen(room: Room<State>, token: string | undefined, addr: string): void {
|
||||
let seat: Seat;
|
||||
try {
|
||||
seat = this.seatOf(room, token);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (seat.addr === addr) return;
|
||||
const line: LedgerLine = { t: 'seen', at: Date.now(), id: seat.id, addr };
|
||||
this.store.append(room.id, line);
|
||||
this.apply(room, line);
|
||||
}
|
||||
|
||||
/**
|
||||
* The seats a name holds at tables not yet over, for a browser that lost them. A seat last used from the
|
||||
* asker's own address, at a table where no other person shares that address, is theirs again with a new
|
||||
* token, the old one dead; the rest are named, so the asker can get a phrase from the host there.
|
||||
*/
|
||||
findSeats(rawName: string, addr: string, only?: string): { seated: { roomId: string; seat: SeatId; token: string }[]; elsewhere: { roomId: string; seat: SeatId; players: string[] }[] } {
|
||||
const name = cleanName(rawName).toLowerCase();
|
||||
const seated: { roomId: string; seat: SeatId; token: string }[] = [];
|
||||
const elsewhere: { roomId: string; seat: SeatId; players: string[] }[] = [];
|
||||
const since = Date.now() - LOST_SEAT_LOOKBACK_MS;
|
||||
const ids = only ? [normalizeCode(String(only))] : this.store.roomIds().filter((id) => this.store.updatedAt(id) > since);
|
||||
for (const id of ids) {
|
||||
let room: Room<State>;
|
||||
try {
|
||||
room = this.get(id);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (room.over) continue;
|
||||
const person = (s: Seat) => !s.bot || s.standIn;
|
||||
const mine = room.seats.find((s) => person(s) && s.name.toLowerCase() === name);
|
||||
if (!mine) continue;
|
||||
const shared = room.seats.some((s) => s !== mine && person(s) && s.addr === addr);
|
||||
if (mine.addr === addr && !shared) {
|
||||
const { token } = this.reseat(room, mine.id);
|
||||
seated.push({ roomId: room.id, seat: mine.id, token });
|
||||
} else elsewhere.push({ roomId: room.id, seat: mine.id, players: room.seats.filter((s) => s !== mine).map((s) => s.name) });
|
||||
}
|
||||
return { seated, elsewhere };
|
||||
}
|
||||
|
||||
/** Claim a phrase: the seat and its token, once. Too many misses void every pending phrase. */
|
||||
claim(raw: unknown): { roomId: string; seat: SeatId; token: string } {
|
||||
const now = Date.now();
|
||||
@@ -714,6 +764,11 @@ export class Rooms<State, Input> {
|
||||
case 'seat':
|
||||
room.seats.push({ id: line.id, name: line.name, tokenHash: line.tokenHash, bot: line.bot });
|
||||
break;
|
||||
case 'seen': {
|
||||
const seat = room.seats.find((s) => s.id === line.id);
|
||||
if (seat) seat.addr = line.addr;
|
||||
break;
|
||||
}
|
||||
case 'reseat': {
|
||||
const seat = room.seats.find((s) => s.id === line.id);
|
||||
if (!seat) break;
|
||||
|
||||
Reference in new issue
Block a user