Stirling-PDF/frontend/src/contexts/FileContext.tsx
Ludy 02189a67bd
refactor(frontend): remove unused React default imports (#4529)
## Description of Changes

- Removed unused `React` default imports across multiple frontend
components.
- Updated imports to only include required React hooks and types (e.g.,
`useState`, `useEffect`, `Suspense`, `createContext`).
- Ensured consistency with React 17+ JSX transform, where default
`React` import is no longer required.
- This cleanup reduces bundle size slightly and aligns code with modern
React best practices.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2025-09-29 13:01:09 +01:00

275 lines
9.4 KiB
TypeScript

/**
* FileContext - Manages PDF files for Stirling PDF multi-tool workflow
*
* Handles file state, memory management, and resource cleanup for large PDFs (up to 100GB+).
* Users upload PDFs once and chain tools (split → merge → compress → view) without reloading.
*
* Key hooks:
* - useFileState() - access file state and UI state
* - useFileActions() - file operations (add/remove/update)
* - useFileSelection() - for file selection state and actions
*
* Memory management handled by FileLifecycleManager (PDF.js cleanup, blob URL revocation).
*/
import { useReducer, useCallback, useEffect, useRef, useMemo } from 'react';
import {
FileContextProviderProps,
FileContextSelectors,
FileContextStateValue,
FileContextActionsValue,
FileContextActions,
FileId,
StirlingFileStub,
StirlingFile,
} from '../types/fileContext';
// Import modular components
import { fileContextReducer, initialFileContextState } from './file/FileReducer';
import { createFileSelectors } from './file/fileSelectors';
import { addFiles, addStirlingFileStubs, consumeFiles, undoConsumeFiles, createFileActions } from './file/fileActions';
import { FileLifecycleManager } from './file/lifecycle';
import { FileStateContext, FileActionsContext } from './file/contexts';
import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext';
const DEBUG = process.env.NODE_ENV === 'development';
// Inner provider component that has access to IndexedDB
function FileContextInner({
children,
enablePersistence = true
}: FileContextProviderProps) {
const [state, dispatch] = useReducer(fileContextReducer, initialFileContextState);
// IndexedDB context for persistence
const indexedDB = enablePersistence ? useIndexedDB() : null;
// File ref map - stores File objects outside React state
const filesRef = useRef<Map<FileId, File>>(new Map());
// Stable state reference for selectors
const stateRef = useRef(state);
stateRef.current = state;
// Create lifecycle manager
const lifecycleManagerRef = useRef<FileLifecycleManager | null>(null);
if (!lifecycleManagerRef.current) {
lifecycleManagerRef.current = new FileLifecycleManager(filesRef, dispatch);
}
const lifecycleManager = lifecycleManagerRef.current;
// Create stable selectors (memoized once to avoid re-renders)
const selectors = useMemo<FileContextSelectors>(() =>
createFileSelectors(stateRef, filesRef),
[] // Empty deps - selectors are stable
);
// Navigation management removed - moved to NavigationContext
// Navigation guard system functions
const setHasUnsavedChanges = useCallback((hasChanges: boolean) => {
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } });
}, []);
const selectFiles = (stirlingFiles: StirlingFile[]) => {
const currentSelection = stateRef.current.ui.selectedFileIds;
const newFileIds = stirlingFiles.map(stirlingFile => stirlingFile.fileId);
dispatch({ type: 'SET_SELECTED_FILES', payload: { fileIds: [...currentSelection, ...newFileIds] } });
};
// File operations using unified addFiles helper with persistence
const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise<StirlingFile[]> => {
const stirlingFiles = await addFiles({ files, ...options }, stateRef, filesRef, dispatch, lifecycleManager, enablePersistence);
// Auto-select the newly added files if requested
if (options?.selectFiles && stirlingFiles.length > 0) {
selectFiles(stirlingFiles);
}
return stirlingFiles;
}, [enablePersistence]);
const addStirlingFileStubsAction = useCallback(async (stirlingFileStubs: StirlingFileStub[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise<StirlingFile[]> => {
// StirlingFileStubs preserve all metadata - perfect for FileManager use case!
const result = await addStirlingFileStubs(stirlingFileStubs, options, stateRef, filesRef, dispatch, lifecycleManager);
// Auto-select the newly added files if requested
if (options?.selectFiles && result.length > 0) {
selectFiles(result);
}
return result;
}, []);
// Action creators
const baseActions = useMemo(() => createFileActions(dispatch), []);
// Helper functions for pinned files
const consumeFilesWrapper = useCallback(async (inputFileIds: FileId[], outputStirlingFiles: StirlingFile[], outputStirlingFileStubs: StirlingFileStub[]): Promise<FileId[]> => {
return consumeFiles(inputFileIds, outputStirlingFiles, outputStirlingFileStubs, filesRef, dispatch);
}, []);
const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[]): Promise<void> => {
return undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds, filesRef, dispatch, indexedDB);
}, [indexedDB]);
// File pinning functions - use StirlingFile directly
const pinFileWrapper = useCallback((file: StirlingFile) => {
baseActions.pinFile(file.fileId);
}, [baseActions]);
const unpinFileWrapper = useCallback((file: StirlingFile) => {
baseActions.unpinFile(file.fileId);
}, [baseActions]);
// Complete actions object
const actions = useMemo<FileContextActions>(() => ({
...baseActions,
addFiles: addRawFiles,
addStirlingFileStubs: addStirlingFileStubsAction,
removeFiles: async (fileIds: FileId[], deleteFromStorage?: boolean) => {
// Remove from memory and cleanup resources
lifecycleManager.removeFiles(fileIds, stateRef);
// Remove from IndexedDB if enabled
if (indexedDB && enablePersistence && deleteFromStorage !== false) {
try {
await indexedDB.deleteMultiple(fileIds);
} catch (error) {
console.error('Failed to delete files from IndexedDB:', error);
}
}
},
updateStirlingFileStub: (fileId: FileId, updates: Partial<StirlingFileStub>) =>
lifecycleManager.updateStirlingFileStub(fileId, updates, stateRef),
reorderFiles: (orderedFileIds: FileId[]) => {
dispatch({ type: 'REORDER_FILES', payload: { orderedFileIds } });
},
clearAllFiles: async () => {
lifecycleManager.cleanupAllFiles();
filesRef.current.clear();
dispatch({ type: 'RESET_CONTEXT' });
// Don't clear IndexedDB automatically - only clear in-memory state
// IndexedDB should only be cleared when explicitly requested by user
},
clearAllData: async () => {
// First clear all files from memory
lifecycleManager.cleanupAllFiles();
filesRef.current.clear();
dispatch({ type: 'RESET_CONTEXT' });
// Then clear IndexedDB storage
if (indexedDB && enablePersistence) {
try {
await indexedDB.clearAll();
} catch (error) {
console.error('Failed to clear IndexedDB:', error);
}
}
},
// Pinned files functionality with File object wrappers
pinFile: pinFileWrapper,
unpinFile: unpinFileWrapper,
consumeFiles: consumeFilesWrapper,
undoConsumeFiles: undoConsumeFilesWrapper,
setHasUnsavedChanges,
trackBlobUrl: lifecycleManager.trackBlobUrl,
cleanupFile: (fileId: FileId) => lifecycleManager.cleanupFile(fileId, stateRef),
scheduleCleanup: (fileId: FileId, delay?: number) =>
lifecycleManager.scheduleCleanup(fileId, delay, stateRef)
}), [
baseActions,
addRawFiles,
addStirlingFileStubsAction,
lifecycleManager,
setHasUnsavedChanges,
consumeFilesWrapper,
undoConsumeFilesWrapper,
pinFileWrapper,
unpinFileWrapper,
indexedDB,
enablePersistence
]);
// Split context values to minimize re-renders
const stateValue = useMemo<FileContextStateValue>(() => ({
state,
selectors
}), [state, selectors]);
const actionsValue = useMemo<FileContextActionsValue>(() => ({
actions,
dispatch
}), [actions]);
// Persistence loading disabled - files only loaded on explicit user action
// useEffect(() => {
// if (!enablePersistence || !indexedDB) return;
// const loadFromPersistence = async () => { /* loading logic removed */ };
// loadFromPersistence();
// }, [enablePersistence, indexedDB]);
// Cleanup on unmount
useEffect(() => {
return () => {
if (DEBUG) console.log('FileContext unmounting - cleaning up all resources');
lifecycleManager.destroy();
};
}, [lifecycleManager]);
return (
<FileStateContext.Provider value={stateValue}>
<FileActionsContext.Provider value={actionsValue}>
{children}
</FileActionsContext.Provider>
</FileStateContext.Provider>
);
}
// Outer provider component that wraps with IndexedDBProvider
export function FileContextProvider({
children,
enableUrlSync = true,
enablePersistence = true
}: FileContextProviderProps) {
if (enablePersistence) {
return (
<IndexedDBProvider>
<FileContextInner
enableUrlSync={enableUrlSync}
enablePersistence={enablePersistence}
>
{children}
</FileContextInner>
</IndexedDBProvider>
);
} else {
return (
<FileContextInner
enableUrlSync={enableUrlSync}
enablePersistence={enablePersistence}
>
{children}
</FileContextInner>
);
}
}
// Export all hooks from the fileHooks module
export {
useFileState,
useFileActions,
useCurrentFile,
useFileSelection,
useFileManagement,
useFileUI,
useStirlingFileStub,
useAllFiles,
useSelectedFiles,
// Primary API hooks for tools
useFileContext
} from './file/fileHooks';