From fcd98b6a4d3fc613eaeb4a8b0387669e2e8d6bd5 Mon Sep 17 00:00:00 2001 From: Jonathan Sykes Date: Wed, 7 Oct 2026 16:30:01 +0800 Subject: [PATCH] Add queue prewarm candidate selection --- frontend/prewarm-next.js | 37 +++++++++++++++++++++++++++++++++++ frontend/prewarm-next.test.js | 26 ++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 frontend/prewarm-next.js create mode 100644 frontend/prewarm-next.test.js diff --git a/frontend/prewarm-next.js b/frontend/prewarm-next.js new file mode 100644 index 0000000..ec56f35 --- /dev/null +++ b/frontend/prewarm-next.js @@ -0,0 +1,37 @@ +/* Pure queue candidate selection for stream prewarming. */ +(function (root, factory) { + if (typeof module === 'object' && module.exports) module.exports = factory(); + else root.PrewarmNext = factory(); +})(typeof globalThis !== 'undefined' ? globalThis : this, function () { + 'use strict'; + + function candidates(queue, index, state = {}, n = 2, known = {}) { + if (!Array.isArray(queue) || !queue.length || !Number.isInteger(index) || index < 0 || index >= queue.length) return []; + if (state.loopOne) return []; + const count = Math.max(0, Math.floor(Number(n) || 0)); + if (!count) return []; + const repeatAll = state.repeatMode === 'all' || state.repeatAll === true; + const offline = new Set(known.offline || []); + const serverCached = new Set(known.serverCached || []); + const current = queue[index]; + const currentId = typeof current === 'string' ? current : current && current.id; + const seen = new Set(currentId ? [currentId] : []); + const ids = []; + for (let step = 1; step < queue.length + (repeatAll ? 1 : 0) && ids.length < count; step++) { + let at = index + step; + if (at >= queue.length) { + if (!repeatAll) break; + at %= queue.length; + } + const item = queue[at]; + const id = typeof item === 'string' ? item : item && item.id; + if (!id || seen.has(id)) continue; + seen.add(id); + if (offline.has(id) || serverCached.has(id) || (item && item.serverCached)) continue; + ids.push(id); + } + return ids; + } + + return { candidates }; +}); diff --git a/frontend/prewarm-next.test.js b/frontend/prewarm-next.test.js new file mode 100644 index 0000000..9a4f467 --- /dev/null +++ b/frontend/prewarm-next.test.js @@ -0,0 +1,26 @@ +'use strict'; +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { candidates } = require('./prewarm-next.js'); + +const q = ['a', 'b', 'c', 'd'].map(id => ({ id })); + +test('returns the next n queue ids in existing order', () => { + assert.deepEqual(candidates(q, 0, {}, 2), ['b', 'c']); +}); +test('repeat all wraps and does not duplicate ids', () => { + assert.deepEqual(candidates(q, 3, { repeatMode: 'all' }, 2), ['a', 'b']); + assert.deepEqual(candidates([{ id: 'x' }, { id: 'x' }], 0, { repeatMode: 'all' }, 3), []); +}); +test('repeat off stops at the end and loop one has no candidates', () => { + assert.deepEqual(candidates(q, 3, {}, 2), []); + assert.deepEqual(candidates(q, 0, { loopOne: true, repeatMode: 'all' }, 2), []); +}); +test('skips offline and known server cached ids and deduplicates', () => { + assert.deepEqual(candidates(q, 0, {}, 3, { offline: ['b'], serverCached: ['c'] }), ['d']); + assert.deepEqual(candidates([{ id: 'a' }, { id: 'b', serverCached: true }, { id: 'c' }], 0, {}, 2), ['c']); +}); +test('shuffle follows the queue order selected before advance', () => { + const shuffled = ['a', 'd', 'b', 'c'].map(id => ({ id })); + assert.deepEqual(candidates(shuffled, 0, { shuffle: true }, 2), ['d', 'b']); +});