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 <noreply@anthropic.com>
This commit is contained in:
Цвылев Александр Вадимович
2026-07-29 10:53:48 +03:00
parent 7f116f0657
commit 5b31e77b1e
2 changed files with 23 additions and 1 deletions
+6 -1
View File
@@ -8,6 +8,11 @@ import { pickToEntry } from '../../lib/radio';
// the end of the queue. // the end of the queue.
const LOOKAHEAD = 3; 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 * 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 * (§6.5 / B6 infinite feed). Mounted once in AppShell. A failed prefetch just
@@ -34,7 +39,7 @@ export function RadioController() {
fromLikes: radio.fromLikes, fromLikes: radio.fromLikes,
exploration: radio.exploration, exploration: radio.exploration,
count: 10, count: 10,
excludeIds: entries.map((e) => e.trackId), excludeIds: entries.slice(-EXCLUDE_WINDOW).map((e) => e.trackId),
}).unwrap(); }).unwrap();
if (res.picks.length > 0) { if (res.picks.length > 0) {
dispatch(appendToQueue(res.picks.map(pickToEntry))); dispatch(appendToQueue(res.picks.map(pickToEntry)));
+17
View File
@@ -38,6 +38,12 @@ export interface QueueState {
radio: RadioContext | null; 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 = { export const queueInitialState: QueueState = {
entries: [], entries: [],
currentIndex: -1, currentIndex: -1,
@@ -83,6 +89,17 @@ export const queueSlice = createSlice({
}, },
appendToQueue(state, action: PayloadAction<QueueEntry[]>) { appendToQueue(state, action: PayloadAction<QueueEntry[]>) {
state.entries.push(...action.payload); 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<number>) { setExploration(state, action: PayloadAction<number>) {
if (state.radio) state.radio.exploration = action.payload; if (state.radio) state.radio.exploration = action.payload;