// Lazy-loading page cache with 5-page ahead prefetch // Shared by both comic and manga readers // Lazy-loading page cache with 5-page ahead prefetch // Procedural implementation (no OOP) import { detectPanels } from "./panel-detection.service"; interface PageCacheState { cache: Map; loading: Set; maxAhead: number; mediaItemId: string; panelData: Map; } function createPageCache(mediaItemId: string): PageCacheState { return { cache: new Map(), loading: new Set(), maxAhead: 5, mediaItemId, panelData: new Map(), }; } async function getCachedPage( state: PageCacheState, pageNumber: number, ): Promise { if (state.cache.has(pageNumber)) { return { ...state, page: state.cache.get(pageNumber)! }; } if (state.loading.has(pageNumber)) { return new Promise((resolve) => { const checkInterval = setInterval(() => { if (state.cache.has(pageNumber)) { clearInterval(checkInterval); resolve({ ...state, page: state.cache.get(pageNumber)! }); } }, 100); }) as Promise; } const newLoading = new Set(state.loading); newLoading.add(pageNumber); const img = await loadComicPage(state, pageNumber); const newCache = new Map(state.cache); newCache.set(pageNumber, img); newLoading.delete(pageNumber); const newState = { ...state, cache: newCache, loading: newLoading }; prefetchPages(newState, pageNumber + 1); cleanupPageCache(newState, pageNumber); return { ...newState, page: img }; } async function loadComicPage( state: PageCacheState, pageNumber: number, ): Promise { const token = localStorage.getItem("token"); const response = await fetch( `/readers/${state.mediaItemId}/pages/${pageNumber}`, { headers: { Authorization: `Bearer ${token}` }, }, ); if (!response.ok) { throw new Error(`Failed to load page ${pageNumber}`); } const blob = await response.blob(); const img = new Image(); img.src = URL.createObjectURL(blob); await new Promise((resolve) => { img.onload = resolve; }); return img; } function prefetchPages(state: PageCacheState, startPage: number): void { for (let i = startPage; i < startPage + state.maxAhead; i++) { if (!state.cache.has(i) && !state.loading.has(i)) { loadComicPage(state, i).then((img) => { state.cache.set(i, img); }); } } } function cleanupPageCache( state: PageCacheState, currentPage: number, ): PageCacheState { const keepPages = 10; const newCache = new Map(state.cache); for (const [page] of state.cache) { if (page < currentPage - keepPages) { newCache.delete(page); } } return { ...state, cache: newCache }; } // Add this function async function detectPagePanels( state: PageCacheState, pageNumber: number, ): Promise { // Check if already detected if (state.panelData?.has(pageNumber)) { return state.panelData.get(pageNumber)!.panels; } // Get or create image let image: HTMLImageElement; if (state.cache.has(pageNumber)) { image = state.cache.get(pageNumber)!; } else { image = await loadComicPage(state, pageNumber); } // Run detection on demand const canvas = document.createElement("canvas"); canvas.width = image.width; canvas.height = image.height; const ctx = canvas.getContext("2d")!; ctx.drawImage(image, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const result = await detectPanels(imageData, true); if (!state.panelData) { state.panelData = new Map(); } state.panelData.set(pageNumber, result); return result.panels; } // Export the new function export { createPageCache, getCachedPage, loadComicPage, detectPagePanels };