fix(frontend): bound Call-to-Play projection work
A valid remote view could make the renderer reduce and mount thousands of calls every second. A single long chat also re-sorted its growing message array after every append during every reduction. Keep only the highest-ranked 128 nominations and latest 256 messages for UI projection. Events are sorted once before folding, so chat messages remain in stable order without repeated growing-array sorts. Backend author history and creator authority are unchanged. Test Plan: - `just frontend-test` -- passed; 94 frontend tests passed. - `just fmt` -- TypeScript formatting completed; the recipe then hit the pre-existing generated security-report Markdown-lint failures. - `git diff --cached --check` -- passed.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()] };
|
||||
|
||||
Reference in New Issue
Block a user