From f6167d2d949bd37a8b190f886e7914c960ddea70 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sat, 14 Mar 2026 11:30:12 -0700 Subject: [PATCH] allow 3 send winner msgs, in case a client reconnects --- src/server/ClientMsgRateLimiter.ts | 11 +++++++---- tests/server/ClientMsgRateLimiter.test.ts | 6 ++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/server/ClientMsgRateLimiter.ts b/src/server/ClientMsgRateLimiter.ts index 8e52a2028..986149da9 100644 --- a/src/server/ClientMsgRateLimiter.ts +++ b/src/server/ClientMsgRateLimiter.ts @@ -7,11 +7,14 @@ const MAX_BYTES_PER_MINUTE = 25 * 1024; // 25KB/min per client const MAX_INTENT_BYTES = 500; // intents are stored in turns, keep them small export type RateLimitResult = "ok" | "limit" | "kick"; +// Allow 3 winner messages per client since a player can rejoin and resend. +const MAX_WINNER_MSGS = 3; + interface ClientBucket { perSecond: RateLimiter; perMinute: RateLimiter; bytesPerMinute: RateLimiter; - hasSentWinnerMsg: boolean; + winnerMsgCount: number; } export class ClientMsgRateLimiter { @@ -23,8 +26,8 @@ export class ClientMsgRateLimiter { // Winner message contains stats for all players and can be large (100s of KB). // It bypasses the byte rate limit but is strictly limited to one per client. if (type === "winner") { - if (bucket.hasSentWinnerMsg) return "kick"; - bucket.hasSentWinnerMsg = true; + if (bucket.winnerMsgCount >= MAX_WINNER_MSGS) return "kick"; + bucket.winnerMsgCount++; return "ok"; } @@ -61,7 +64,7 @@ export class ClientMsgRateLimiter { tokensPerInterval: MAX_BYTES_PER_MINUTE, interval: "minute", }), - hasSentWinnerMsg: false, + winnerMsgCount: 0, }; this.buckets.set(clientID, bucket); return bucket; diff --git a/tests/server/ClientMsgRateLimiter.test.ts b/tests/server/ClientMsgRateLimiter.test.ts index ab70f5ef5..263464485 100644 --- a/tests/server/ClientMsgRateLimiter.test.ts +++ b/tests/server/ClientMsgRateLimiter.test.ts @@ -42,9 +42,11 @@ describe("ClientMsgRateLimiter", () => { expect(limiter.check(CLIENT_A, "winner", 50000)).toBe("ok"); }); - it("kicks on second winner message", () => { + it("allows up to 3 winner messages", () => { const limiter = new ClientMsgRateLimiter(); - limiter.check(CLIENT_A, "winner", 50000); + expect(limiter.check(CLIENT_A, "winner", 50000)).toBe("ok"); + expect(limiter.check(CLIENT_A, "winner", 50000)).toBe("ok"); + expect(limiter.check(CLIENT_A, "winner", 50000)).toBe("ok"); expect(limiter.check(CLIENT_A, "winner", 50000)).toBe("kick"); });