mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 11:00:47 +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:
@@ -3,13 +3,15 @@
|
||||
* FileContext uses pure File objects with separate ID tracking
|
||||
*/
|
||||
|
||||
declare const tag: unique symbol;
|
||||
export type FileId = string & { readonly [tag]: 'FileId' };
|
||||
|
||||
/**
|
||||
* File metadata for efficient operations without loading full file data
|
||||
* Used by IndexedDBContext and FileContext for lazy file loading
|
||||
*/
|
||||
export interface FileMetadata {
|
||||
id: string;
|
||||
id: FileId;
|
||||
name: string;
|
||||
type: string;
|
||||
size: number;
|
||||
@@ -36,9 +38,9 @@ export const defaultStorageConfig: StorageConfig = {
|
||||
export const initializeStorageConfig = async (): Promise<StorageConfig> => {
|
||||
const tenGB = 10 * 1024 * 1024 * 1024; // 10GB in bytes
|
||||
const oneGB = 1024 * 1024 * 1024; // 1GB fallback
|
||||
|
||||
|
||||
let maxTotalStorage = oneGB; // Default fallback
|
||||
|
||||
|
||||
// Try to estimate available storage
|
||||
if ('storage' in navigator && 'estimate' in navigator.storage) {
|
||||
try {
|
||||
@@ -51,9 +53,9 @@ export const initializeStorageConfig = async (): Promise<StorageConfig> => {
|
||||
console.warn('Could not estimate storage quota, using 1GB default:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
...defaultStorageConfig,
|
||||
maxTotalStorage
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
* Types for global file context management across views and tools
|
||||
*/
|
||||
|
||||
import { ProcessedFile } from './processing';
|
||||
import { PDFDocument, PDFPage, PageOperation } from './pageEditor';
|
||||
import { FileMetadata } from './file';
|
||||
import { PageOperation } from './pageEditor';
|
||||
import { FileId, FileMetadata } from './file';
|
||||
|
||||
export type ModeType =
|
||||
| 'viewer'
|
||||
@@ -26,8 +25,6 @@ export type ModeType =
|
||||
| 'removeCertificateSign';
|
||||
|
||||
// Normalized state types
|
||||
export type FileId = string;
|
||||
|
||||
export interface ProcessedFilePage {
|
||||
thumbnail?: string;
|
||||
pageNumber?: number;
|
||||
@@ -69,14 +66,14 @@ export interface FileContextNormalizedFiles {
|
||||
export function createFileId(): FileId {
|
||||
// Use crypto.randomUUID for authoritative primary key
|
||||
if (typeof window !== 'undefined' && window.crypto?.randomUUID) {
|
||||
return window.crypto.randomUUID();
|
||||
return window.crypto.randomUUID() as FileId;
|
||||
}
|
||||
// Fallback for environments without randomUUID
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}) as FileId;
|
||||
}
|
||||
|
||||
// Generate quick deduplication key from file metadata
|
||||
@@ -136,7 +133,7 @@ export interface FileOperation {
|
||||
id: string;
|
||||
type: OperationType;
|
||||
timestamp: number;
|
||||
fileIds: string[];
|
||||
fileIds: FileId[];
|
||||
status: 'pending' | 'applied' | 'failed';
|
||||
data?: any;
|
||||
metadata?: {
|
||||
@@ -150,7 +147,7 @@ export interface FileOperation {
|
||||
}
|
||||
|
||||
export interface FileOperationHistory {
|
||||
fileId: string;
|
||||
fileId: FileId;
|
||||
fileName: string;
|
||||
operations: (FileOperation | PageOperation)[];
|
||||
createdAt: number;
|
||||
@@ -165,7 +162,7 @@ export interface ViewerConfig {
|
||||
}
|
||||
|
||||
export interface FileEditHistory {
|
||||
fileId: string;
|
||||
fileId: FileId;
|
||||
pageOperations: PageOperation[];
|
||||
lastModified: number;
|
||||
}
|
||||
@@ -176,10 +173,10 @@ export interface FileContextState {
|
||||
ids: FileId[];
|
||||
byId: Record<FileId, FileRecord>;
|
||||
};
|
||||
|
||||
// Pinned files - files that won't be consumed by tools
|
||||
|
||||
// Pinned files - files that won't be consumed by tools
|
||||
pinnedFiles: Set<FileId>;
|
||||
|
||||
|
||||
// UI state - file-related UI state only
|
||||
ui: {
|
||||
selectedFileIds: FileId[];
|
||||
@@ -191,27 +188,27 @@ export interface FileContextState {
|
||||
}
|
||||
|
||||
// Action types for reducer pattern
|
||||
export type FileContextAction =
|
||||
export type FileContextAction =
|
||||
// File management actions
|
||||
| { type: 'ADD_FILES'; payload: { fileRecords: FileRecord[] } }
|
||||
| { type: 'REMOVE_FILES'; payload: { fileIds: FileId[] } }
|
||||
| { type: 'UPDATE_FILE_RECORD'; payload: { id: FileId; updates: Partial<FileRecord> } }
|
||||
| { type: 'REORDER_FILES'; payload: { orderedFileIds: FileId[] } }
|
||||
|
||||
|
||||
// Pinned files actions
|
||||
| { type: 'PIN_FILE'; payload: { fileId: FileId } }
|
||||
| { type: 'UNPIN_FILE'; payload: { fileId: FileId } }
|
||||
| { type: 'CONSUME_FILES'; payload: { inputFileIds: FileId[]; outputFileRecords: FileRecord[] } }
|
||||
|
||||
// UI actions
|
||||
|
||||
// UI actions
|
||||
| { type: 'SET_SELECTED_FILES'; payload: { fileIds: FileId[] } }
|
||||
| { type: 'SET_SELECTED_PAGES'; payload: { pageNumbers: number[] } }
|
||||
| { type: 'CLEAR_SELECTIONS' }
|
||||
| { type: 'SET_PROCESSING'; payload: { isProcessing: boolean; progress: number } }
|
||||
|
||||
|
||||
// Navigation guard actions (minimal for file-related unsaved changes only)
|
||||
| { type: 'SET_UNSAVED_CHANGES'; payload: { hasChanges: boolean } }
|
||||
|
||||
|
||||
// Context management
|
||||
| { type: 'RESET_CONTEXT' };
|
||||
|
||||
@@ -236,20 +233,20 @@ export interface FileContextActions {
|
||||
setSelectedFiles: (fileIds: FileId[]) => void;
|
||||
setSelectedPages: (pageNumbers: number[]) => void;
|
||||
clearSelections: () => void;
|
||||
|
||||
|
||||
// Processing state - simple flags only
|
||||
setProcessing: (isProcessing: boolean, progress?: number) => void;
|
||||
|
||||
|
||||
// File-related unsaved changes (minimal navigation guard support)
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
||||
|
||||
|
||||
// Context management
|
||||
resetContext: () => void;
|
||||
|
||||
|
||||
// Resource management
|
||||
trackBlobUrl: (url: string) => void;
|
||||
scheduleCleanup: (fileId: string, delay?: number) => void;
|
||||
cleanupFile: (fileId: string) => void;
|
||||
scheduleCleanup: (fileId: FileId, delay?: number) => void;
|
||||
cleanupFile: (fileId: FileId) => void;
|
||||
}
|
||||
|
||||
// File selectors (separate from actions to avoid re-renders)
|
||||
@@ -257,22 +254,22 @@ export interface FileContextSelectors {
|
||||
// File access - no state dependency, uses ref
|
||||
getFile: (id: FileId) => File | undefined;
|
||||
getFiles: (ids?: FileId[]) => File[];
|
||||
|
||||
|
||||
// Record access - uses normalized state
|
||||
getFileRecord: (id: FileId) => FileRecord | undefined;
|
||||
getFileRecords: (ids?: FileId[]) => FileRecord[];
|
||||
|
||||
|
||||
// Derived selectors
|
||||
getAllFileIds: () => FileId[];
|
||||
getSelectedFiles: () => File[];
|
||||
getSelectedFileRecords: () => FileRecord[];
|
||||
|
||||
|
||||
// Pinned files selectors
|
||||
getPinnedFileIds: () => FileId[];
|
||||
getPinnedFiles: () => File[];
|
||||
getPinnedFileRecords: () => FileRecord[];
|
||||
isFilePinned: (file: File) => boolean;
|
||||
|
||||
|
||||
// Stable signature for effect dependencies
|
||||
getFilesSignature: () => string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { FileId } from './file';
|
||||
|
||||
export interface PDFPage {
|
||||
id: string;
|
||||
pageNumber: number;
|
||||
@@ -7,7 +9,7 @@ export interface PDFPage {
|
||||
selected: boolean;
|
||||
splitAfter?: boolean;
|
||||
isBlankPage?: boolean;
|
||||
originalFileId?: string;
|
||||
originalFileId?: FileId;
|
||||
}
|
||||
|
||||
export interface PDFDocument {
|
||||
|
||||
Reference in New Issue
Block a user