From 5b31e77b1edec314609c12d460772bc59e8006b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A6=D0=B2=D1=8B=D0=BB=D0=B5=D0=B2=20=D0=90=D0=BB=D0=B5?= =?UTF-8?q?=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20=D0=92=D0=B0=D0=B4=D0=B8?= =?UTF-8?q?=D0=BC=D0=BE=D0=B2=D0=B8=D1=87?= Date: Wed, 29 Jul 2026 10:53:48 +0300 Subject: [PATCH] perf(radio): bound queue backlog and exclude list An infinite radio appends forever: cap the queue at QUEUE_CAP, trimming only already-played entries (keep KEEP_BEHIND for prev-nav), and send only the most recent EXCLUDE_WINDOW ids as "don't repeat" so the request and server NOT IN don't grow unbounded. Co-Authored-By: Claude Opus 4.8 --- src/components/player/RadioController.tsx | 7 ++++++- src/store/slices/queue.ts | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/components/player/RadioController.tsx b/src/components/player/RadioController.tsx index fe48182..b7f24ca 100644 --- a/src/components/player/RadioController.tsx +++ b/src/components/player/RadioController.tsx @@ -8,6 +8,11 @@ import { pickToEntry } from '../../lib/radio'; // the end of the queue. const LOOKAHEAD = 3; +// Only the most recent tracks are sent as "don't repeat these" — bounding the +// request so a long radio session doesn't ship an ever-growing exclude list +// (and a huge NOT IN on the server). Recent-repeat avoidance is what matters. +const EXCLUDE_WINDOW = 150; + /** * Headless controller: while a radio drives the queue, top it up as it drains * (§6.5 / B6 infinite feed). Mounted once in AppShell. A failed prefetch just @@ -34,7 +39,7 @@ export function RadioController() { fromLikes: radio.fromLikes, exploration: radio.exploration, count: 10, - excludeIds: entries.map((e) => e.trackId), + excludeIds: entries.slice(-EXCLUDE_WINDOW).map((e) => e.trackId), }).unwrap(); if (res.picks.length > 0) { dispatch(appendToQueue(res.picks.map(pickToEntry))); diff --git a/src/store/slices/queue.ts b/src/store/slices/queue.ts index 1f20134..a5843e8 100644 --- a/src/store/slices/queue.ts +++ b/src/store/slices/queue.ts @@ -38,6 +38,12 @@ export interface QueueState { radio: RadioContext | null; } +// An infinite radio keeps appending, so cap the backlog: once the queue grows +// past QUEUE_CAP, drop the oldest *already-played* entries (never the current +// track or the look-ahead), keeping at least KEEP_BEHIND for prev-navigation. +const QUEUE_CAP = 300; +const KEEP_BEHIND = 50; + export const queueInitialState: QueueState = { entries: [], currentIndex: -1, @@ -83,6 +89,17 @@ export const queueSlice = createSlice({ }, appendToQueue(state, action: PayloadAction) { state.entries.push(...action.payload); + if (state.entries.length > QUEUE_CAP) { + // Only trim entries safely behind the current one. + const drop = Math.min( + state.entries.length - QUEUE_CAP, + Math.max(0, state.currentIndex - KEEP_BEHIND), + ); + if (drop > 0) { + state.entries.splice(0, drop); + state.currentIndex -= drop; + } + } }, setExploration(state, action: PayloadAction) { if (state.radio) state.radio.exploration = action.payload;