From 210db2d5c366bbc5e507b7e9fbbaa6111ce3de35 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Tue, 10 Nov 2026 21:32:27 -0500 Subject: [PATCH] The template notes a seat's address at most once in ten minutes, so a seat open on two networks writes no line per request Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CCLa2PaEeCj8njQZSWGqHx --- template/server/src/rooms.test.ts | 2 ++ template/server/src/rooms.ts | 13 +++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/template/server/src/rooms.test.ts b/template/server/src/rooms.test.ts index 4533151..34673e2 100644 --- a/template/server/src/rooms.test.ts +++ b/template/server/src/rooms.test.ts @@ -337,6 +337,8 @@ describe('a seat a browser lost', () => { expect(room.seats.find((s) => s.id === 'B')?.addr).toBe('home'); expect(room.seq).toBe(seq); rooms.seen(room, 'no-such-token', 'elsewhere'); + // A seat open on two networks at once: the second address is not written on its heels. + rooms.seen(room, bo, 'office'); expect(new Store(dir).read(room.id).filter((l) => l.t === 'seen')).toHaveLength(1); expect(new Rooms(game, new Store(dir)).get(room.id).seats.find((s) => s.id === 'B')?.addr).toBe('home'); }); diff --git a/template/server/src/rooms.ts b/template/server/src/rooms.ts index 4849cd6..b76835c 100644 --- a/template/server/src/rooms.ts +++ b/template/server/src/rooms.ts @@ -36,6 +36,8 @@ export interface Seat { /** 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; +/** A seat's address is noted at most this often: one seat open on two networks at once must not write a line per request. */ +const SEEN_EVERY_MS = 10 * 60 * 1000; function hashToken(token: string): string { return createHash('sha256').update(token).digest('hex'); @@ -323,7 +325,10 @@ export class Rooms { 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. */ + /** When each seat's address was last noted, by room and seat. */ + private seenAt = new Map(); + + /** The address a seat is used from, noted when it changes, and not more often than SEEN_EVERY_MS. Not a change the table need hear of: nothing wakes for it. */ seen(room: Room, token: string | undefined, addr: string): void { let seat: Seat; try { @@ -332,7 +337,11 @@ export class Rooms { return; } if (seat.addr === addr) return; - const line: LedgerLine = { t: 'seen', at: Date.now(), id: seat.id, addr }; + const key = `${room.id}/${seat.id}`; + const now = Date.now(); + if ((this.seenAt.get(key) ?? 0) > now - SEEN_EVERY_MS) return; + this.seenAt.set(key, now); + const line: LedgerLine = { t: 'seen', at: now, id: seat.id, addr }; this.store.append(room.id, line); this.apply(room, line); }