mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
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:
@@ -1,14 +1,14 @@
|
||||
/**
|
||||
* 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)
|
||||
* - useFileActions() - file operations (add/remove/update)
|
||||
* - useFileSelection() - for file selection state and actions
|
||||
*
|
||||
*
|
||||
* Memory management handled by FileLifecycleManager (PDF.js cleanup, blob URL revocation).
|
||||
*/
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
FileContextStateValue,
|
||||
FileContextActionsValue,
|
||||
FileContextActions,
|
||||
FileId,
|
||||
FileRecord
|
||||
} from '../types/fileContext';
|
||||
|
||||
@@ -30,6 +29,7 @@ import { addFiles, consumeFiles, createFileActions } from './file/fileActions';
|
||||
import { FileLifecycleManager } from './file/lifecycle';
|
||||
import { FileStateContext, FileActionsContext } from './file/contexts';
|
||||
import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext';
|
||||
import { FileId } from '../types/file';
|
||||
|
||||
const DEBUG = process.env.NODE_ENV === 'development';
|
||||
|
||||
@@ -38,16 +38,16 @@ const DEBUG = process.env.NODE_ENV === 'development';
|
||||
function FileContextInner({
|
||||
children,
|
||||
enableUrlSync = true,
|
||||
enablePersistence = true
|
||||
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;
|
||||
@@ -60,8 +60,8 @@ function FileContextInner({
|
||||
const lifecycleManager = lifecycleManagerRef.current;
|
||||
|
||||
// Create stable selectors (memoized once to avoid re-renders)
|
||||
const selectors = useMemo<FileContextSelectors>(() =>
|
||||
createFileSelectors(stateRef, filesRef),
|
||||
const selectors = useMemo<FileContextSelectors>(() =>
|
||||
createFileSelectors(stateRef, filesRef),
|
||||
[] // Empty deps - selectors are stable
|
||||
);
|
||||
|
||||
@@ -75,7 +75,6 @@ function FileContextInner({
|
||||
// File operations using unified addFiles helper with persistence
|
||||
const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string }): Promise<File[]> => {
|
||||
const addedFilesWithIds = await addFiles('raw', { files, ...options }, stateRef, filesRef, dispatch, lifecycleManager);
|
||||
|
||||
// Persist to IndexedDB if enabled
|
||||
if (indexedDB && enablePersistence && addedFilesWithIds.length > 0) {
|
||||
await Promise.all(addedFilesWithIds.map(async ({ file, id, thumbnail }) => {
|
||||
@@ -86,7 +85,7 @@ function FileContextInner({
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
return addedFilesWithIds.map(({ file }) => file);
|
||||
}, [indexedDB, enablePersistence]);
|
||||
|
||||
@@ -110,11 +109,11 @@ function FileContextInner({
|
||||
|
||||
// Helper to find FileId from File object
|
||||
const findFileId = useCallback((file: File): FileId | undefined => {
|
||||
return Object.keys(stateRef.current.files.byId).find(id => {
|
||||
return (Object.keys(stateRef.current.files.byId) as FileId[]).find(id => {
|
||||
const storedFile = filesRef.current.get(id);
|
||||
return storedFile &&
|
||||
storedFile.name === file.name &&
|
||||
storedFile.size === file.size &&
|
||||
return storedFile &&
|
||||
storedFile.name === file.name &&
|
||||
storedFile.size === file.size &&
|
||||
storedFile.lastModified === file.lastModified;
|
||||
});
|
||||
}, []);
|
||||
@@ -143,11 +142,11 @@ function FileContextInner({
|
||||
...baseActions,
|
||||
addFiles: addRawFiles,
|
||||
addProcessedFiles,
|
||||
addStoredFiles,
|
||||
addStoredFiles,
|
||||
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 {
|
||||
@@ -157,7 +156,7 @@ function FileContextInner({
|
||||
}
|
||||
}
|
||||
},
|
||||
updateFileRecord: (fileId: FileId, updates: Partial<FileRecord>) =>
|
||||
updateFileRecord: (fileId: FileId, updates: Partial<FileRecord>) =>
|
||||
lifecycleManager.updateFileRecord(fileId, updates, stateRef),
|
||||
reorderFiles: (orderedFileIds: FileId[]) => {
|
||||
dispatch({ type: 'REORDER_FILES', payload: { orderedFileIds } });
|
||||
@@ -166,7 +165,7 @@ function FileContextInner({
|
||||
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
|
||||
},
|
||||
@@ -175,7 +174,7 @@ function FileContextInner({
|
||||
lifecycleManager.cleanupAllFiles();
|
||||
filesRef.current.clear();
|
||||
dispatch({ type: 'RESET_CONTEXT' });
|
||||
|
||||
|
||||
// Then clear IndexedDB storage
|
||||
if (indexedDB && enablePersistence) {
|
||||
try {
|
||||
@@ -191,14 +190,14 @@ function FileContextInner({
|
||||
consumeFiles: consumeFilesWrapper,
|
||||
setHasUnsavedChanges,
|
||||
trackBlobUrl: lifecycleManager.trackBlobUrl,
|
||||
cleanupFile: (fileId: string) => lifecycleManager.cleanupFile(fileId, stateRef),
|
||||
scheduleCleanup: (fileId: string, delay?: number) =>
|
||||
cleanupFile: (fileId: FileId) => lifecycleManager.cleanupFile(fileId, stateRef),
|
||||
scheduleCleanup: (fileId: FileId, delay?: number) =>
|
||||
lifecycleManager.scheduleCleanup(fileId, delay, stateRef)
|
||||
}), [
|
||||
baseActions,
|
||||
addRawFiles,
|
||||
addProcessedFiles,
|
||||
addStoredFiles,
|
||||
baseActions,
|
||||
addRawFiles,
|
||||
addProcessedFiles,
|
||||
addStoredFiles,
|
||||
lifecycleManager,
|
||||
setHasUnsavedChanges,
|
||||
consumeFilesWrapper,
|
||||
@@ -247,12 +246,12 @@ function FileContextInner({
|
||||
export function FileContextProvider({
|
||||
children,
|
||||
enableUrlSync = true,
|
||||
enablePersistence = true
|
||||
enablePersistence = true
|
||||
}: FileContextProviderProps) {
|
||||
if (enablePersistence) {
|
||||
return (
|
||||
<IndexedDBProvider>
|
||||
<FileContextInner
|
||||
<FileContextInner
|
||||
enableUrlSync={enableUrlSync}
|
||||
enablePersistence={enablePersistence}
|
||||
>
|
||||
@@ -262,7 +261,7 @@ export function FileContextProvider({
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<FileContextInner
|
||||
<FileContextInner
|
||||
enableUrlSync={enableUrlSync}
|
||||
enablePersistence={enablePersistence}
|
||||
>
|
||||
@@ -285,4 +284,4 @@ export {
|
||||
useSelectedFiles,
|
||||
// Primary API hooks for tools
|
||||
useFileContext
|
||||
} from './file/fileHooks';
|
||||
} from './file/fileHooks';
|
||||
|
||||
Reference in New Issue
Block a user