V2 Make FileId type opaque and use consistently throughout project (#4307)

# Description of Changes
The `FileId` type in V2 currently is just defined to be a string. This
makes it really easy to accidentally pass strings into things accepting
file IDs (such as file names). This PR makes the `FileId` type [an
opaque
type](https://www.geeksforgeeks.org/typescript/opaque-types-in-typescript/),
so it is compatible with things accepting strings (arguably not ideal
for this...) but strings are not compatible with it without explicit
conversion.

The PR also includes changes to use `FileId` consistently throughout the
project (everywhere I could find uses of `fileId: string`), so that we
have the maximum benefit from the type safety.

> [!note]
> I've marked quite a few things as `FIX ME` where we're passing names
in as IDs. If that is intended behaviour, I'm happy to remove the fix me
and insert a cast instead, but they probably need comments explaining
why we're using a file name as an ID.
This commit is contained in:
James Brunton
2025-08-28 09:56:07 +00:00
committed by GitHub
parent 581bafbd37
commit e142af2863
32 changed files with 600 additions and 574 deletions
+26 -25
View File
@@ -1,5 +1,6 @@
import { useCallback, useRef } from 'react';
import { thumbnailGenerationService } from '../services/thumbnailGenerationService';
import { FileId } from '../types/file';
// Request queue to handle concurrent thumbnail requests
interface ThumbnailRequest {
@@ -35,44 +36,44 @@ async function processRequestQueue() {
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 file name as fileId for PDF document caching
const fileId = file.name + '_' + file.size + '_' + file.lastModified;
const fileId = file.name + '_' + file.size + '_' + file.lastModified as FileId;
const results = await thumbnailGenerationService.generateThumbnails(
fileId,
arrayBuffer,
@@ -83,11 +84,11 @@ async function processRequestQueue() {
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);
@@ -96,7 +97,7 @@ async function processRequestQueue() {
request.resolve(null);
}
}
} catch (error) {
console.warn(`Batch thumbnail generation failed for ${requests.length} pages:`, error);
// Reject all requests in this batch
@@ -115,7 +116,7 @@ async function processRequestQueue() {
*/
export function useThumbnailGeneration() {
const generateThumbnails = useCallback(async (
fileId: string,
fileId: FileId,
pdfArrayBuffer: ArrayBuffer,
pageNumbers: number[],
options: {
@@ -157,22 +158,22 @@ export function useThumbnailGeneration() {
clearTimeout(batchTimer);
batchTimer = null;
}
// Clear the queue and active requests
requestQueue.length = 0;
activeRequests.clear();
isProcessingQueue = false;
thumbnailGenerationService.destroy();
}, []);
const clearPDFCacheForFile = useCallback((fileId: string) => {
const clearPDFCacheForFile = useCallback((fileId: FileId) => {
thumbnailGenerationService.clearPDFCacheForFile(fileId);
}, []);
const requestThumbnail = useCallback(async (
pageId: string,
file: File,
pageId: string,
file: File,
pageNumber: number
): Promise<string | null> => {
// Check cache first for immediate return
@@ -202,16 +203,16 @@ export function useThumbnailGeneration() {
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);
@@ -222,7 +223,7 @@ export function useThumbnailGeneration() {
// Track this request to prevent duplicates
activeRequests.set(pageId, requestPromise);
return requestPromise;
}, []);
@@ -236,4 +237,4 @@ export function useThumbnailGeneration() {
clearPDFCacheForFile,
requestThumbnail
};
}
}