import { createPlaceholderCube, setConfirmedInterval } from "./block-cube.js"; import { olderRemaining, olderRunway } from "./scroll.js"; const OLDER_RESERVE_VIEWPORTS = 6; /** * @typedef {import("../../modules/brk-client/index.js").BlockInfoV1} Block * @typedef {{ generation: number, startHeight: number, placeholders: HTMLElement[] }} OlderBatch */ /** * @param {Object} args * @param {HTMLElement} args.scrollElement * @param {HTMLElement} args.blocksElement * @param {number} args.batchSize * @param {() => boolean} args.isActive * @param {() => boolean} args.isHorizontal * @param {(startHeight: number) => Promise} args.fetchBlocks * @param {(block: Block) => HTMLButtonElement} args.createCube * @param {() => boolean} args.isAborted * @param {(error: unknown) => void} args.onError */ export function createOlderBlocks({ scrollElement, blocksElement, batchSize, isActive, isHorizontal, fetchBlocks, createCube, isAborted, onError, }) { let oldestHeight = Infinity; let oldestReservedHeight = -1; let hydrating = false; let generation = 0; /** @type {OlderBatch[]} */ const batches = []; function reset() { oldestHeight = Infinity; oldestReservedHeight = -1; hydrating = false; generation++; batches.length = 0; } /** @param {number} height */ function setOldestHeight(height) { oldestHeight = height; oldestReservedHeight = height; } /** * @param {Element | null} anchor * @param {number} count */ function prependPlaceholders(anchor, count) { const fragment = document.createDocumentFragment(); const placeholders = /** @type {HTMLElement[]} */ ([]); for (let i = 0; i < count; i++) { const cube = createPlaceholderCube(); placeholders.push(cube); fragment.append(cube); } blocksElement.insertBefore(fragment, anchor); return placeholders; } function reserveBatch() { if (!isActive() || oldestReservedHeight <= 0) return false; const anchor = blocksElement.firstElementChild; const count = Math.min(batchSize, oldestReservedHeight); const startHeight = oldestReservedHeight - 1; const placeholders = prependPlaceholders(anchor, count); if (!placeholders.length) return false; oldestReservedHeight -= placeholders.length; batches.push({ generation, startHeight, placeholders }); void hydrateBatches(); return true; } /** @param {number} [delta] */ function reserve(delta = 0) { if (!isActive() || oldestReservedHeight <= 0) return; const horizontal = isHorizontal(); const runway = olderRunway(scrollElement, horizontal, OLDER_RESERVE_VIEWPORTS) + delta; let remaining = olderRemaining(scrollElement, horizontal); while (remaining < runway) { if (!reserveBatch()) return; remaining = olderRemaining(scrollElement, horizontal); } } async function hydrateBatches() { if (hydrating) return; const currentGeneration = generation; hydrating = true; try { while ( isActive() && currentGeneration === generation && batches[0]?.generation === currentGeneration ) { await hydrateBatch(batches[0]); if (batches[0]?.generation === currentGeneration) batches.shift(); } } finally { if (currentGeneration === generation) hydrating = false; } } /** @param {OlderBatch} batch */ async function hydrateBatch(batch) { try { const blocks = await fetchBlocks(batch.startHeight); if ( batch.generation !== generation || !batch.placeholders.some((placeholder) => placeholder.isConnected) ) { return; } const cubes = [...blocks].reverse().map(createCube); for (let i = 0; i < batch.placeholders.length; i++) { const cube = cubes[i]; if (cube) batch.placeholders[i].replaceWith(cube); else batch.placeholders[i].remove(); } for (const cube of cubes) setConfirmedInterval(cube); const next = cubes.at(-1)?.nextElementSibling; if (next instanceof HTMLElement) setConfirmedInterval(next); if (blocks.length) { oldestHeight = blocks[blocks.length - 1].height; } else { oldestReservedHeight = oldestHeight; } reserve(); } catch (error) { if (isAborted() || batch.generation !== generation) return; for (const placeholder of batch.placeholders) placeholder.remove(); oldestReservedHeight = oldestHeight; onError(error); } } return /** @type {const} */ ({ reserve, reset, setOldestHeight, }); }