diff --git a/crates/lanspread-tauri-deno-ts/src/lib/callToPlay.ts b/crates/lanspread-tauri-deno-ts/src/lib/callToPlay.ts index c262cdb..f8c0218 100644 --- a/crates/lanspread-tauri-deno-ts/src/lib/callToPlay.ts +++ b/crates/lanspread-tauri-deno-ts/src/lib/callToPlay.ts @@ -9,6 +9,8 @@ import { export const CHECKIN_LEAD_MS = 15 * 60_000; export const EXPIRED_RETENTION_MS = 5 * 60_000; export const TERMINAL_RETENTION_MS = 15 * 60_000; +export const MAX_VISIBLE_CALL_TO_PLAY_NOMINATIONS = 128; +export const MAX_VISIBLE_CALL_TO_PLAY_MESSAGES = 256; export const CALL_TO_PLAY_CONNECTING_MESSAGE = 'Call to Play is still connecting to the LAN. Try again in a moment.'; @@ -138,7 +140,7 @@ export const reduceCallToPlayEvents = ( const nominations = [...groupEvents(input).values()] .map(events => deriveNomination(events, now)) .filter((nomination): nomination is Nomination => nomination !== null); - return sortNominations(nominations); + return sortNominations(nominations).slice(0, MAX_VISIBLE_CALL_TO_PLAY_NOMINATIONS); }; const groupEvents = ( @@ -189,6 +191,9 @@ const deriveNomination = ( for (const event of events) { if (compareEvents(event, create) > 0) applyEvent(nomination, event); } + if (nomination.messages.length > MAX_VISIBLE_CALL_TO_PLAY_MESSAGES) { + nomination.messages = nomination.messages.slice(-MAX_VISIBLE_CALL_TO_PLAY_MESSAGES); + } if (nomination.state === 'open' && (readyCountOf(nomination, now) >= nomination.maxPlayers || now >= nomination.deadline) @@ -237,7 +242,6 @@ const applyEvent = (nomination: MutableNomination, event: CallToPlayViewEvent): text: message.text, at: event.at, }); - nomination.messages.sort((a, b) => a.at - b.at || a.id.localeCompare(b.id)); return; } diff --git a/crates/lanspread-tauri-deno-ts/tests/callToPlay.test.ts b/crates/lanspread-tauri-deno-ts/tests/callToPlay.test.ts index a3bfe4d..e87a9e4 100644 --- a/crates/lanspread-tauri-deno-ts/tests/callToPlay.test.ts +++ b/crates/lanspread-tauri-deno-ts/tests/callToPlay.test.ts @@ -2,6 +2,8 @@ import { CALL_TO_PLAY_CONNECTING_MESSAGE, CHECKIN_LEAD_MS, EXPIRED_RETENTION_MS, + MAX_VISIBLE_CALL_TO_PLAY_MESSAGES, + MAX_VISIBLE_CALL_TO_PLAY_NOMINATIONS, TERMINAL_RETENTION_MS, activeCallCount, callToPlayPublishErrorMessage, @@ -291,6 +293,40 @@ Deno.test('terminal calls sort last and do not contribute to the badge', () => { assertEquals(statusOf(cancelled, NOW + 3), 'cancelled', 'cancelled ticker status'); }); +Deno.test('large Call to Play views are bounded to the ranked UI subset', () => { + const events = Array.from({ length: MAX_VISIBLE_CALL_TO_PLAY_NOMINATIONS + 1 }, (_, index) => ({ + ...create(), + id: `create-${index}`, + call_id: `call-${index}`, + })); + + assertEquals( + reduceCallToPlayEvents(events, NOW).length, + MAX_VISIBLE_CALL_TO_PLAY_NOMINATIONS, + 'rendered nominations must stay within the UI budget', + ); +}); + +Deno.test('one call exposes only the latest bounded chat history', () => { + const messages = Array.from({ length: MAX_VISIBLE_CALL_TO_PLAY_MESSAGES + 1 }, (_, index) => + event( + `message-${index}`, + 'Bob', + { SendMessage: { text: `message ${index}` } }, + NOW + index + 1, + ) + ); + + const [nomination] = reduceCallToPlayEvents([create(), ...messages], NOW + messages.length + 1); + + assertEquals( + nomination.messages.length, + MAX_VISIBLE_CALL_TO_PLAY_MESSAGES, + 'visible messages must stay within the UI budget', + ); + assertEquals(nomination.messages[0].text, 'message 1', 'oldest excess message is removed'); +}); + Deno.test('incoming Call to Play views replace removed author slices wholesale', () => { const previous = { events: [create(), event('join', 'Bob', 'Rsvp', NOW + 1)] }; const incoming = { events: [create()] };