mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Clean up file management
This commit is contained in:
parent
3a5402b55a
commit
a260d72925
@ -74,7 +74,7 @@ function FileContextInner({
|
||||
|
||||
// File operations using unified addFiles helper with persistence
|
||||
const addRawFiles = useCallback(async (files: File[]): Promise<File[]> => {
|
||||
const addedFilesWithIds = await addFiles('raw', { files }, stateRef, filesRef, dispatch);
|
||||
const addedFilesWithIds = await addFiles('raw', { files }, stateRef, filesRef, dispatch, lifecycleManager);
|
||||
|
||||
// Persist to IndexedDB if enabled - pass existing thumbnail to prevent double generation
|
||||
if (indexedDB && enablePersistence && addedFilesWithIds.length > 0) {
|
||||
@ -91,12 +91,12 @@ function FileContextInner({
|
||||
}, [indexedDB, enablePersistence]);
|
||||
|
||||
const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise<File[]> => {
|
||||
const result = await addFiles('processed', { filesWithThumbnails }, stateRef, filesRef, dispatch);
|
||||
const result = await addFiles('processed', { filesWithThumbnails }, stateRef, filesRef, dispatch, lifecycleManager);
|
||||
return result.map(({ file }) => file);
|
||||
}, []);
|
||||
|
||||
const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>): Promise<File[]> => {
|
||||
const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch);
|
||||
const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager);
|
||||
return result.map(({ file }) => file);
|
||||
}, []);
|
||||
|
||||
@ -179,7 +179,6 @@ function FileContextInner({
|
||||
consumeFiles: consumeFilesWrapper,
|
||||
setHasUnsavedChanges,
|
||||
trackBlobUrl: lifecycleManager.trackBlobUrl,
|
||||
trackPdfDocument: lifecycleManager.trackPdfDocument,
|
||||
cleanupFile: (fileId: string) => lifecycleManager.cleanupFile(fileId, stateRef),
|
||||
scheduleCleanup: (fileId: string, delay?: number) =>
|
||||
lifecycleManager.scheduleCleanup(fileId, delay, stateRef)
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
} from '../../types/fileContext';
|
||||
import { FileMetadata } from '../../types/file';
|
||||
import { generateThumbnailWithMetadata } from '../../utils/thumbnailUtils';
|
||||
import { FileLifecycleManager } from './lifecycle';
|
||||
import { fileProcessingService } from '../../services/fileProcessingService';
|
||||
import { buildQuickKeySet, buildQuickKeySetFromMetadata } from './fileSelectors';
|
||||
|
||||
@ -59,7 +60,8 @@ export async function addFiles(
|
||||
options: AddFileOptions,
|
||||
stateRef: React.MutableRefObject<FileContextState>,
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||
dispatch: React.Dispatch<FileContextAction>
|
||||
dispatch: React.Dispatch<FileContextAction>,
|
||||
lifecycleManager: FileLifecycleManager
|
||||
): Promise<Array<{ file: File; id: FileId; thumbnail?: string }>> {
|
||||
const fileRecords: FileRecord[] = [];
|
||||
const addedFiles: Array<{ file: File; id: FileId; thumbnail?: string }> = [];
|
||||
@ -104,6 +106,10 @@ export async function addFiles(
|
||||
const record = toFileRecord(file, fileId);
|
||||
if (thumbnail) {
|
||||
record.thumbnailUrl = thumbnail;
|
||||
// Track blob URLs for cleanup (images return blob URLs that need revocation)
|
||||
if (thumbnail.startsWith('blob:')) {
|
||||
lifecycleManager.trackBlobUrl(thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
// Create initial processedFile metadata with page count
|
||||
@ -140,6 +146,10 @@ export async function addFiles(
|
||||
const record = toFileRecord(file, fileId);
|
||||
if (thumbnail) {
|
||||
record.thumbnailUrl = thumbnail;
|
||||
// Track blob URLs for cleanup (images return blob URLs that need revocation)
|
||||
if (thumbnail.startsWith('blob:')) {
|
||||
lifecycleManager.trackBlobUrl(thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
// Create processedFile with provided metadata
|
||||
@ -182,6 +192,10 @@ export async function addFiles(
|
||||
// Restore metadata from storage
|
||||
if (metadata.thumbnail) {
|
||||
record.thumbnailUrl = metadata.thumbnail;
|
||||
// Track blob URLs for cleanup (images return blob URLs that need revocation)
|
||||
if (metadata.thumbnail.startsWith('blob:')) {
|
||||
lifecycleManager.trackBlobUrl(metadata.thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate processedFile metadata for stored files using PDF worker manager
|
||||
|
@ -150,7 +150,6 @@ export function useFileContext() {
|
||||
return useMemo(() => ({
|
||||
// Lifecycle management
|
||||
trackBlobUrl: actions.trackBlobUrl,
|
||||
trackPdfDocument: actions.trackPdfDocument,
|
||||
scheduleCleanup: actions.scheduleCleanup,
|
||||
setUnsavedChanges: actions.setHasUnsavedChanges,
|
||||
|
||||
|
@ -12,7 +12,6 @@ const DEBUG = process.env.NODE_ENV === 'development';
|
||||
export class FileLifecycleManager {
|
||||
private cleanupTimers = new Map<string, number>();
|
||||
private blobUrls = new Set<string>();
|
||||
private pdfDocuments = new Map<string, any>();
|
||||
private fileGenerations = new Map<string, number>(); // Generation tokens to prevent stale cleanup
|
||||
|
||||
constructor(
|
||||
@ -33,24 +32,6 @@ export class FileLifecycleManager {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Track PDF documents for cleanup
|
||||
*/
|
||||
trackPdfDocument = (key: string, pdfDoc: any): void => {
|
||||
// Clean up existing PDF document if present
|
||||
const existing = this.pdfDocuments.get(key);
|
||||
if (existing && typeof existing.destroy === 'function') {
|
||||
try {
|
||||
existing.destroy();
|
||||
if (DEBUG) console.log(`🗂️ Destroyed existing PDF document for key: ${key}`);
|
||||
} catch (error) {
|
||||
if (DEBUG) console.warn('Error destroying existing PDF document:', error);
|
||||
}
|
||||
}
|
||||
|
||||
this.pdfDocuments.set(key, pdfDoc);
|
||||
if (DEBUG) console.log(`🗂️ Tracking PDF document for key: ${key}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean up resources for a specific file (with stateRef access for complete cleanup)
|
||||
@ -71,19 +52,6 @@ export class FileLifecycleManager {
|
||||
cleanupAllFiles = (): void => {
|
||||
if (DEBUG) console.log('🗂️ Cleaning up all files and resources');
|
||||
|
||||
// Clean up all PDF documents
|
||||
this.pdfDocuments.forEach((pdfDoc, key) => {
|
||||
if (pdfDoc && typeof pdfDoc.destroy === 'function') {
|
||||
try {
|
||||
pdfDoc.destroy();
|
||||
if (DEBUG) console.log(`🗂️ Destroyed PDF document for key: ${key}`);
|
||||
} catch (error) {
|
||||
if (DEBUG) console.warn(`Error destroying PDF document for key ${key}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.pdfDocuments.clear();
|
||||
|
||||
// Revoke all blob URLs
|
||||
this.blobUrls.forEach(url => {
|
||||
try {
|
||||
@ -162,22 +130,6 @@ export class FileLifecycleManager {
|
||||
// Remove from files ref
|
||||
this.filesRef.current.delete(fileId);
|
||||
|
||||
// Clean up PDF documents (scan all keys that start with fileId)
|
||||
const keysToDelete: string[] = [];
|
||||
this.pdfDocuments.forEach((pdfDoc, key) => {
|
||||
if (key === fileId || key.startsWith(`${fileId}:`)) {
|
||||
if (pdfDoc && typeof pdfDoc.destroy === 'function') {
|
||||
try {
|
||||
pdfDoc.destroy();
|
||||
keysToDelete.push(key);
|
||||
if (DEBUG) console.log(`🗂️ Destroyed PDF document for key: ${key}`);
|
||||
} catch (error) {
|
||||
if (DEBUG) console.warn(`Error destroying PDF document for key ${key}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
keysToDelete.forEach(key => this.pdfDocuments.delete(key));
|
||||
|
||||
// Cancel cleanup timer and generation
|
||||
const timer = this.cleanupTimers.get(fileId);
|
||||
|
@ -1,30 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useFileContext } from '../contexts/FileContext';
|
||||
|
||||
/**
|
||||
* Hook for components that need to register resources with centralized memory management
|
||||
*/
|
||||
export function useMemoryManagement() {
|
||||
const { trackBlobUrl, trackPdfDocument, scheduleCleanup } = useFileContext();
|
||||
|
||||
const registerBlobUrl = useCallback((url: string) => {
|
||||
trackBlobUrl(url);
|
||||
return url;
|
||||
}, [trackBlobUrl]);
|
||||
|
||||
const registerPdfDocument = useCallback((fileId: string, pdfDoc: any) => {
|
||||
trackPdfDocument(fileId, pdfDoc);
|
||||
return pdfDoc;
|
||||
}, [trackPdfDocument]);
|
||||
|
||||
const cancelCleanup = useCallback((fileId: string) => {
|
||||
// Cancel scheduled cleanup (user is actively using the file)
|
||||
scheduleCleanup(fileId, -1); // -1 cancels the timer
|
||||
}, [scheduleCleanup]);
|
||||
|
||||
return {
|
||||
registerBlobUrl,
|
||||
registerPdfDocument,
|
||||
cancelCleanup
|
||||
};
|
||||
}
|
@ -182,42 +182,44 @@ export class EnhancedPDFProcessingService {
|
||||
): Promise<ProcessedFile> {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdf = await pdfWorkerManager.createDocument(arrayBuffer);
|
||||
const totalPages = pdf.numPages;
|
||||
|
||||
state.progress = 10;
|
||||
this.notifyListeners();
|
||||
try {
|
||||
const totalPages = pdf.numPages;
|
||||
|
||||
const pages: PDFPage[] = [];
|
||||
state.progress = 10;
|
||||
this.notifyListeners();
|
||||
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
// Check for cancellation
|
||||
if (state.cancellationToken?.signal.aborted) {
|
||||
pdfWorkerManager.destroyDocument(pdf);
|
||||
throw new Error('Processing cancelled');
|
||||
const pages: PDFPage[] = [];
|
||||
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
// Check for cancellation
|
||||
if (state.cancellationToken?.signal.aborted) {
|
||||
throw new Error('Processing cancelled');
|
||||
}
|
||||
|
||||
const page = await pdf.getPage(i);
|
||||
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
|
||||
|
||||
pages.push({
|
||||
id: `${file.name}-page-${i}`,
|
||||
pageNumber: i,
|
||||
thumbnail,
|
||||
rotation: 0,
|
||||
selected: false
|
||||
});
|
||||
|
||||
// Update progress
|
||||
state.progress = 10 + (i / totalPages) * 85;
|
||||
state.currentPage = i;
|
||||
this.notifyListeners();
|
||||
}
|
||||
|
||||
const page = await pdf.getPage(i);
|
||||
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
|
||||
|
||||
pages.push({
|
||||
id: `${file.name}-page-${i}`,
|
||||
pageNumber: i,
|
||||
thumbnail,
|
||||
rotation: 0,
|
||||
selected: false
|
||||
});
|
||||
|
||||
// Update progress
|
||||
state.progress = 10 + (i / totalPages) * 85;
|
||||
state.currentPage = i;
|
||||
return this.createProcessedFile(file, pages, totalPages);
|
||||
} finally {
|
||||
pdfWorkerManager.destroyDocument(pdf);
|
||||
state.progress = 100;
|
||||
this.notifyListeners();
|
||||
}
|
||||
|
||||
pdfWorkerManager.destroyDocument(pdf);
|
||||
state.progress = 100;
|
||||
this.notifyListeners();
|
||||
|
||||
return this.createProcessedFile(file, pages, totalPages);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -23,6 +23,15 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({ files, setDownloadUrl, pa
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("merge-pdfs");
|
||||
|
||||
// Cleanup blob URL when component unmounts or new URL is set
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (downloadUrl && downloadUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
}
|
||||
};
|
||||
}, [downloadUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedFiles(files.map(() => true));
|
||||
}, [files]);
|
||||
@ -67,6 +76,12 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({ files, setDownloadUrl, pa
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
|
||||
// Clean up previous blob URL before setting new one
|
||||
if (downloadUrl && downloadUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
setDownloadUrl(url);
|
||||
setLocalDownloadUrl(url);
|
||||
|
@ -245,7 +245,6 @@ export interface FileContextActions {
|
||||
|
||||
// Resource management
|
||||
trackBlobUrl: (url: string) => void;
|
||||
trackPdfDocument: (key: string, pdfDoc: any) => void;
|
||||
scheduleCleanup: (fileId: string, delay?: number) => void;
|
||||
cleanupFile: (fileId: string) => void;
|
||||
}
|
||||
|
@ -333,7 +333,7 @@ export async function generateThumbnailForFile(file: File): Promise<string | und
|
||||
return generatePlaceholderThumbnail(file);
|
||||
}
|
||||
|
||||
// Handle image files
|
||||
// Handle image files - creates blob URL that needs cleanup by caller
|
||||
if (file.type.startsWith('image/')) {
|
||||
return URL.createObjectURL(file);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user