Stirling-PDF/frontend/src/hooks/useThumbnailGeneration.ts
James Brunton bd13f6bf57
Enable ESLint no-unused-vars rule (#4367)
# Description of Changes
Enable ESLint [no-unused-vars
rule](https://typescript-eslint.io/rules/no-unused-vars/)
2025-09-05 11:16:17 +00:00

242 lines
7.7 KiB
TypeScript

import { useCallback } from 'react';
import { thumbnailGenerationService } from '../services/thumbnailGenerationService';
import { createQuickKey } from '../types/fileContext';
import { FileId } from '../types/file';
// Request queue to handle concurrent thumbnail requests
interface ThumbnailRequest {
pageId: string;
file: File;
pageNumber: number;
resolve: (thumbnail: string | null) => void;
reject: (error: Error) => void;
}
// Global request queue (shared across all hook instances)
const requestQueue: ThumbnailRequest[] = [];
let isProcessingQueue = false;
let batchTimer: number | null = null;
// Track active thumbnail requests to prevent duplicates across components
const activeRequests = new Map<string, Promise<string | null>>();
// Batch processing configuration
const BATCH_SIZE = 20; // Process thumbnails in batches of 20 for better UI responsiveness
const BATCH_DELAY = 100; // Wait 100ms to collect requests before processing
const PRIORITY_BATCH_DELAY = 50; // Faster processing for the first batch (visible pages)
// Process the queue in batches for better performance
async function processRequestQueue() {
if (isProcessingQueue || requestQueue.length === 0) {
return;
}
isProcessingQueue = true;
try {
while (requestQueue.length > 0) {
// Sort queue by page number to prioritize visible pages first
requestQueue.sort((a, b) => a.pageNumber - b.pageNumber);
// Take a batch of requests (same file only for efficiency)
const batchSize = Math.min(BATCH_SIZE, requestQueue.length);
const batch = requestQueue.splice(0, batchSize);
// Group by file to process efficiently
const fileGroups = new Map<File, ThumbnailRequest[]>();
// First, resolve any cached thumbnails immediately
const uncachedRequests: ThumbnailRequest[] = [];
for (const request of batch) {
const cached = thumbnailGenerationService.getThumbnailFromCache(request.pageId);
if (cached) {
request.resolve(cached);
} else {
uncachedRequests.push(request);
if (!fileGroups.has(request.file)) {
fileGroups.set(request.file, []);
}
fileGroups.get(request.file)!.push(request);
}
}
// Process each file group with batch thumbnail generation
for (const [file, requests] of fileGroups) {
if (requests.length === 0) continue;
try {
const pageNumbers = requests.map(req => req.pageNumber);
const arrayBuffer = await file.arrayBuffer();
console.log(`📸 Batch generating ${requests.length} thumbnails for pages: ${pageNumbers.slice(0, 5).join(', ')}${pageNumbers.length > 5 ? '...' : ''}`);
// Use quickKey for PDF document caching (same metadata, consistent format)
const fileId = createQuickKey(file) as FileId;
const results = await thumbnailGenerationService.generateThumbnails(
fileId,
arrayBuffer,
pageNumbers,
{ scale: 1.0, quality: 0.8, batchSize: BATCH_SIZE },
(progress) => {
// Optional: Could emit progress events here for UI feedback
console.log(`📸 Batch progress: ${progress.completed}/${progress.total} thumbnails generated`);
}
);
// Match results back to requests and resolve
for (const request of requests) {
const result = results.find(r => r.pageNumber === request.pageNumber);
if (result && result.success && result.thumbnail) {
thumbnailGenerationService.addThumbnailToCache(request.pageId, result.thumbnail);
request.resolve(result.thumbnail);
} else {
console.warn(`No result for page ${request.pageNumber}`);
request.resolve(null);
}
}
} catch (error) {
console.warn(`Batch thumbnail generation failed for ${requests.length} pages:`, error);
// Reject all requests in this batch
requests.forEach(request => request.reject(error as Error));
}
}
}
} finally {
isProcessingQueue = false;
}
}
/**
* Hook for tools that want to use thumbnail generation
* Tools can choose whether to include visual features
*/
export function useThumbnailGeneration() {
const generateThumbnails = useCallback(async (
fileId: FileId,
pdfArrayBuffer: ArrayBuffer,
pageNumbers: number[],
options: {
scale?: number;
quality?: number;
batchSize?: number;
parallelBatches?: number;
} = {},
onProgress?: (progress: { completed: number; total: number; thumbnails: any[] }) => void
) => {
return thumbnailGenerationService.generateThumbnails(
fileId,
pdfArrayBuffer,
pageNumbers,
options,
onProgress
);
}, []);
const addThumbnailToCache = useCallback((pageId: string, thumbnail: string) => {
thumbnailGenerationService.addThumbnailToCache(pageId, thumbnail);
}, []);
const getThumbnailFromCache = useCallback((pageId: string): string | null => {
return thumbnailGenerationService.getThumbnailFromCache(pageId);
}, []);
const getCacheStats = useCallback(() => {
return thumbnailGenerationService.getCacheStats();
}, []);
const stopGeneration = useCallback(() => {
thumbnailGenerationService.stopGeneration();
}, []);
const destroyThumbnails = useCallback(() => {
// Clear any pending batch timer
if (batchTimer) {
clearTimeout(batchTimer);
batchTimer = null;
}
// Clear the queue and active requests
requestQueue.length = 0;
activeRequests.clear();
isProcessingQueue = false;
thumbnailGenerationService.destroy();
}, []);
const clearPDFCacheForFile = useCallback((fileId: FileId) => {
thumbnailGenerationService.clearPDFCacheForFile(fileId);
}, []);
const requestThumbnail = useCallback(async (
pageId: string,
file: File,
pageNumber: number
): Promise<string | null> => {
// Check cache first for immediate return
const cached = thumbnailGenerationService.getThumbnailFromCache(pageId);
if (cached) {
return cached;
}
// Check if this request is already being processed globally
const activeRequest = activeRequests.get(pageId);
if (activeRequest) {
return activeRequest;
}
// Create new request promise and track it globally
const requestPromise = new Promise<string | null>((resolve, reject) => {
requestQueue.push({
pageId,
file,
pageNumber,
resolve: (result: string | null) => {
activeRequests.delete(pageId);
resolve(result);
},
reject: (error: Error) => {
activeRequests.delete(pageId);
reject(error);
}
});
// Schedule batch processing with a small delay to collect more requests
if (batchTimer) {
clearTimeout(batchTimer);
}
// Use shorter delay for the first batch (pages 1-50) to show visible content faster
const isFirstBatch = requestQueue.length <= BATCH_SIZE && requestQueue.every(req => req.pageNumber <= BATCH_SIZE);
const delay = isFirstBatch ? PRIORITY_BATCH_DELAY : BATCH_DELAY;
batchTimer = window.setTimeout(() => {
processRequestQueue().catch(error => {
console.error('Error processing thumbnail request queue:', error);
});
batchTimer = null;
}, delay);
});
// Track this request to prevent duplicates
activeRequests.set(pageId, requestPromise);
return requestPromise;
}, []);
return {
generateThumbnails,
addThumbnailToCache,
getThumbnailFromCache,
getCacheStats,
stopGeneration,
destroyThumbnails,
clearPDFCacheForFile,
requestThumbnail
};
}