mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 19:10:47 +02:00
Feature/v2/filewithid implementation (#4369)
Added Filewithid type Updated code where file was being used to use filewithid Updated places we identified files by name or composite keys to use UUID Updated places we should have been using quickkey Updated pageeditor issue where we parsed pagenumber from pageid instead of using pagenumber directly --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: James Brunton <[email protected]>
This commit is contained in:
co-authored by
Copilot
James Brunton
parent
5caec41d96
commit
87c63efcec
@@ -5,6 +5,9 @@
|
||||
import { PageOperation } from './pageEditor';
|
||||
import { FileId, FileMetadata } from './file';
|
||||
|
||||
// Re-export FileId for convenience
|
||||
export type { FileId };
|
||||
|
||||
export type ModeType =
|
||||
| 'viewer'
|
||||
| 'pageEditor'
|
||||
@@ -41,25 +44,32 @@ export interface ProcessedFileMetadata {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface FileRecord {
|
||||
id: FileId;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
lastModified: number;
|
||||
quickKey?: string; // Fast deduplication key: name|size|lastModified
|
||||
thumbnailUrl?: string;
|
||||
blobUrl?: string;
|
||||
createdAt?: number;
|
||||
processedFile?: ProcessedFileMetadata;
|
||||
insertAfterPageId?: string; // Page ID after which this file should be inserted
|
||||
isPinned?: boolean;
|
||||
/**
|
||||
* StirlingFileStub - Metadata record for files in the active workbench session
|
||||
*
|
||||
* Contains UI display data and processing state. Actual File objects stored
|
||||
* separately in refs for memory efficiency. Supports multi-tool workflows
|
||||
* where files persist across tool operations.
|
||||
*/
|
||||
export interface StirlingFileStub {
|
||||
id: FileId; // UUID primary key for collision-free operations
|
||||
name: string; // Display name for UI
|
||||
size: number; // File size for progress indicators
|
||||
type: string; // MIME type for format validation
|
||||
lastModified: number; // Original timestamp for deduplication
|
||||
quickKey?: string; // Fast deduplication key: name|size|lastModified
|
||||
thumbnailUrl?: string; // Generated thumbnail blob URL for visual display
|
||||
blobUrl?: string; // File access blob URL for downloads/processing
|
||||
createdAt?: number; // When added to workbench for sorting
|
||||
processedFile?: ProcessedFileMetadata; // PDF page data and processing results
|
||||
insertAfterPageId?: string; // Page ID after which this file should be inserted
|
||||
isPinned?: boolean; // Protected from tool consumption (replace/remove)
|
||||
// Note: File object stored in provider ref, not in state
|
||||
}
|
||||
|
||||
export interface FileContextNormalizedFiles {
|
||||
ids: FileId[];
|
||||
byId: Record<FileId, FileRecord>;
|
||||
byId: Record<FileId, StirlingFileStub>;
|
||||
}
|
||||
|
||||
// Helper functions - UUID-based primary keys (zero collisions, synchronous)
|
||||
@@ -82,9 +92,68 @@ export function createQuickKey(file: File): string {
|
||||
return `${file.name}|${file.size}|${file.lastModified}`;
|
||||
}
|
||||
|
||||
// Stirling PDF file with embedded UUID - replaces loose File + FileId parameter passing
|
||||
export interface StirlingFile extends File {
|
||||
readonly fileId: FileId;
|
||||
readonly quickKey: string; // Fast deduplication key: name|size|lastModified
|
||||
}
|
||||
|
||||
// Type guard to check if a File object has an embedded fileId
|
||||
export function isStirlingFile(file: File): file is StirlingFile {
|
||||
return 'fileId' in file && typeof (file as any).fileId === 'string' &&
|
||||
'quickKey' in file && typeof (file as any).quickKey === 'string';
|
||||
}
|
||||
|
||||
// Create a StirlingFile from a regular File object
|
||||
export function createStirlingFile(file: File, id?: FileId): StirlingFile {
|
||||
const fileId = id || createFileId();
|
||||
const quickKey = createQuickKey(file);
|
||||
|
||||
// Use Object.defineProperty to add properties while preserving the original File object
|
||||
// This maintains proper method binding and avoids "Illegal invocation" errors
|
||||
Object.defineProperty(file, 'fileId', {
|
||||
value: fileId,
|
||||
writable: false,
|
||||
enumerable: true,
|
||||
configurable: false
|
||||
});
|
||||
|
||||
Object.defineProperty(file, 'quickKey', {
|
||||
value: quickKey,
|
||||
writable: false,
|
||||
enumerable: true,
|
||||
configurable: false
|
||||
});
|
||||
|
||||
return file as StirlingFile;
|
||||
}
|
||||
|
||||
// Extract FileIds from StirlingFile array
|
||||
export function extractFileIds(files: StirlingFile[]): FileId[] {
|
||||
return files.map(file => file.fileId);
|
||||
}
|
||||
|
||||
// Extract regular File objects from StirlingFile array
|
||||
export function extractFiles(files: StirlingFile[]): File[] {
|
||||
return files as File[];
|
||||
}
|
||||
|
||||
// Check if an object is a File or StirlingFile (replaces instanceof File checks)
|
||||
export function isFileObject(obj: any): obj is File | StirlingFile {
|
||||
return obj &&
|
||||
typeof obj.name === 'string' &&
|
||||
typeof obj.size === 'number' &&
|
||||
typeof obj.type === 'string' &&
|
||||
typeof obj.lastModified === 'number' &&
|
||||
typeof obj.arrayBuffer === 'function';
|
||||
}
|
||||
|
||||
|
||||
export function toFileRecord(file: File, id?: FileId): FileRecord {
|
||||
|
||||
export function toStirlingFileStub(
|
||||
file: File,
|
||||
id?: FileId
|
||||
): StirlingFileStub {
|
||||
const fileId = id || createFileId();
|
||||
return {
|
||||
id: fileId,
|
||||
@@ -97,7 +166,7 @@ export function toFileRecord(file: File, id?: FileId): FileRecord {
|
||||
};
|
||||
}
|
||||
|
||||
export function revokeFileResources(record: FileRecord): void {
|
||||
export function revokeFileResources(record: StirlingFileStub): void {
|
||||
// Only revoke blob: URLs to prevent errors on other schemes
|
||||
if (record.thumbnailUrl && record.thumbnailUrl.startsWith('blob:')) {
|
||||
try {
|
||||
@@ -171,7 +240,7 @@ export interface FileContextState {
|
||||
// Core file management - lightweight file IDs only
|
||||
files: {
|
||||
ids: FileId[];
|
||||
byId: Record<FileId, FileRecord>;
|
||||
byId: Record<FileId, StirlingFileStub>;
|
||||
};
|
||||
|
||||
// Pinned files - files that won't be consumed by tools
|
||||
@@ -190,16 +259,16 @@ export interface FileContextState {
|
||||
// Action types for reducer pattern
|
||||
export type FileContextAction =
|
||||
// File management actions
|
||||
| { type: 'ADD_FILES'; payload: { fileRecords: FileRecord[] } }
|
||||
| { type: 'ADD_FILES'; payload: { stirlingFileStubs: StirlingFileStub[] } }
|
||||
| { type: 'REMOVE_FILES'; payload: { fileIds: FileId[] } }
|
||||
| { type: 'UPDATE_FILE_RECORD'; payload: { id: FileId; updates: Partial<FileRecord> } }
|
||||
| { type: 'UPDATE_FILE_RECORD'; payload: { id: FileId; updates: Partial<StirlingFileStub> } }
|
||||
| { 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[] } }
|
||||
| { type: 'UNDO_CONSUME_FILES'; payload: { inputFileRecords: FileRecord[]; outputFileIds: FileId[] } }
|
||||
| { type: 'CONSUME_FILES'; payload: { inputFileIds: FileId[]; outputStirlingFileStubs: StirlingFileStub[] } }
|
||||
| { type: 'UNDO_CONSUME_FILES'; payload: { inputStirlingFileStubs: StirlingFileStub[]; outputFileIds: FileId[] } }
|
||||
|
||||
// UI actions
|
||||
| { type: 'SET_SELECTED_FILES'; payload: { fileIds: FileId[] } }
|
||||
@@ -215,22 +284,22 @@ export type FileContextAction =
|
||||
|
||||
export interface FileContextActions {
|
||||
// File management - lightweight actions only
|
||||
addFiles: (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }) => Promise<File[]>;
|
||||
addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise<File[]>;
|
||||
addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>, options?: { selectFiles?: boolean }) => Promise<File[]>;
|
||||
addFiles: (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }) => Promise<StirlingFile[]>;
|
||||
addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise<StirlingFile[]>;
|
||||
addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>, options?: { selectFiles?: boolean }) => Promise<StirlingFile[]>;
|
||||
removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => Promise<void>;
|
||||
updateFileRecord: (id: FileId, updates: Partial<FileRecord>) => void;
|
||||
updateStirlingFileStub: (id: FileId, updates: Partial<StirlingFileStub>) => void;
|
||||
reorderFiles: (orderedFileIds: FileId[]) => void;
|
||||
clearAllFiles: () => Promise<void>;
|
||||
clearAllData: () => Promise<void>;
|
||||
|
||||
// File pinning
|
||||
pinFile: (file: File) => void;
|
||||
unpinFile: (file: File) => void;
|
||||
// File pinning - accepts StirlingFile for safer type checking
|
||||
pinFile: (file: StirlingFile) => void;
|
||||
unpinFile: (file: StirlingFile) => void;
|
||||
|
||||
// File consumption (replace unpinned files with outputs)
|
||||
consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise<FileId[]>;
|
||||
undoConsumeFiles: (inputFiles: File[], inputFileRecords: FileRecord[], outputFileIds: FileId[]) => Promise<void>;
|
||||
undoConsumeFiles: (inputFiles: File[], inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[]) => Promise<void>;
|
||||
// Selection management
|
||||
setSelectedFiles: (fileIds: FileId[]) => void;
|
||||
setSelectedPages: (pageNumbers: number[]) => void;
|
||||
@@ -253,26 +322,17 @@ export interface FileContextActions {
|
||||
|
||||
// File selectors (separate from actions to avoid re-renders)
|
||||
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
|
||||
getFile: (id: FileId) => StirlingFile | undefined;
|
||||
getFiles: (ids?: FileId[]) => StirlingFile[];
|
||||
getStirlingFileStub: (id: FileId) => StirlingFileStub | undefined;
|
||||
getStirlingFileStubs: (ids?: FileId[]) => StirlingFileStub[];
|
||||
getAllFileIds: () => FileId[];
|
||||
getSelectedFiles: () => File[];
|
||||
getSelectedFileRecords: () => FileRecord[];
|
||||
|
||||
// Pinned files selectors
|
||||
getSelectedFiles: () => StirlingFile[];
|
||||
getSelectedStirlingFileStubs: () => StirlingFileStub[];
|
||||
getPinnedFileIds: () => FileId[];
|
||||
getPinnedFiles: () => File[];
|
||||
getPinnedFileRecords: () => FileRecord[];
|
||||
isFilePinned: (file: File) => boolean;
|
||||
|
||||
// Stable signature for effect dependencies
|
||||
getPinnedFiles: () => StirlingFile[];
|
||||
getPinnedStirlingFileStubs: () => StirlingFileStub[];
|
||||
isFilePinned: (file: StirlingFile) => boolean;
|
||||
getFilesSignature: () => string;
|
||||
}
|
||||
|
||||
@@ -293,6 +353,3 @@ export interface FileContextActionsValue {
|
||||
actions: FileContextActions;
|
||||
dispatch: (action: FileContextAction) => void;
|
||||
}
|
||||
|
||||
// TODO: URL parameter types will be redesigned for new routing system
|
||||
|
||||
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Type safety declarations to prevent file.name/UUID confusion
|
||||
*/
|
||||
|
||||
import { FileId, StirlingFile, OperationType, FileOperation } from './fileContext';
|
||||
|
||||
declare global {
|
||||
namespace FileIdSafety {
|
||||
// Mark functions that should never accept file.name as parameters
|
||||
type SafeFileIdFunction<T extends (...args: any[]) => any> = T extends (...args: infer P) => infer R
|
||||
? P extends readonly [string, ...any[]]
|
||||
? never // Reject string parameters in first position for FileId functions
|
||||
: T
|
||||
: T;
|
||||
|
||||
// Mark functions that should only accept StirlingFile, not regular File
|
||||
type StirlingFileOnlyFunction<T extends (...args: any[]) => any> = T extends (...args: infer P) => infer R
|
||||
? P extends readonly [File, ...any[]]
|
||||
? never // Reject File parameters in first position for StirlingFile functions
|
||||
: T
|
||||
: T;
|
||||
|
||||
// Utility type to enforce StirlingFile usage
|
||||
type RequireStirlingFile<T> = T extends File ? StirlingFile : T;
|
||||
}
|
||||
|
||||
// Extend Window interface for debugging
|
||||
interface Window {
|
||||
__FILE_ID_DEBUG?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
// Augment FileContext types to prevent bypassing StirlingFile
|
||||
declare module '../contexts/FileContext' {
|
||||
export interface StrictFileContextActions {
|
||||
pinFile: (file: StirlingFile) => void; // Must be StirlingFile
|
||||
unpinFile: (file: StirlingFile) => void; // Must be StirlingFile
|
||||
addFiles: (files: File[], options?: { insertAfterPageId?: string }) => Promise<StirlingFile[]>; // Returns StirlingFile
|
||||
consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise<StirlingFile[]>; // Returns StirlingFile
|
||||
}
|
||||
|
||||
export interface StrictFileContextSelectors {
|
||||
getFile: (id: FileId) => StirlingFile | undefined; // Returns StirlingFile
|
||||
getFiles: (ids?: FileId[]) => StirlingFile[]; // Returns StirlingFile[]
|
||||
isFilePinned: (file: StirlingFile) => boolean; // Must be StirlingFile
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
Reference in New Issue
Block a user