2025-09-02 17:24:26 +01:00
|
|
|
/**
|
|
|
|
* File History Utilities
|
2025-09-03 14:48:14 +01:00
|
|
|
*
|
2025-09-10 10:03:35 +01:00
|
|
|
* Helper functions for IndexedDB-based file history management.
|
|
|
|
* Handles file history operations and lineage tracking.
|
2025-09-02 17:24:26 +01:00
|
|
|
*/
|
2025-09-05 17:41:53 +01:00
|
|
|
import { StirlingFileStub } from '../types/fileContext';
|
2025-09-10 10:03:35 +01:00
|
|
|
import { FileId } from '../types/file';
|
|
|
|
import { StoredFileMetadata } from '../services/fileStorage';
|
2025-09-02 17:24:26 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-09-03 14:48:14 +01:00
|
|
|
|
2025-09-02 17:24:26 +01:00
|
|
|
/**
|
2025-09-03 17:47:58 +01:00
|
|
|
* Group files by processing branches - each branch ends in a leaf file
|
|
|
|
* Returns Map<fileId, lineagePath[]> where fileId is the leaf and lineagePath is the path back to original
|
2025-09-02 17:24:26 +01:00
|
|
|
*/
|
2025-09-05 17:41:53 +01:00
|
|
|
export function groupFilesByOriginal(StirlingFileStubs: StirlingFileStub[]): Map<string, StirlingFileStub[]> {
|
|
|
|
const groups = new Map<string, StirlingFileStub[]>();
|
2025-09-02 17:24:26 +01:00
|
|
|
|
2025-09-03 17:47:58 +01:00
|
|
|
// Create a map for quick lookups
|
2025-09-05 17:41:53 +01:00
|
|
|
const fileMap = new Map<string, StirlingFileStub>();
|
|
|
|
for (const record of StirlingFileStubs) {
|
2025-09-03 17:47:58 +01:00
|
|
|
fileMap.set(record.id, record);
|
|
|
|
}
|
2025-09-03 14:48:14 +01:00
|
|
|
|
2025-09-03 17:47:58 +01:00
|
|
|
// Find leaf files (files that are not parents of any other files AND have version history)
|
|
|
|
// Original files (v0) should only be leaves if they have no processed versions at all
|
2025-09-05 17:41:53 +01:00
|
|
|
const leafFiles = StirlingFileStubs.filter(stub => {
|
|
|
|
const isParentOfOthers = StirlingFileStubs.some(otherStub => otherStub.parentFileId === stub.id);
|
|
|
|
const isOriginalOfOthers = StirlingFileStubs.some(otherStub => otherStub.originalFileId === stub.id);
|
|
|
|
|
2025-09-03 17:47:58 +01:00
|
|
|
// A file is a leaf if:
|
|
|
|
// 1. It's not a parent of any other files, AND
|
|
|
|
// 2. It has processing history (versionNumber > 0) OR it's not referenced as original by others
|
2025-09-05 17:41:53 +01:00
|
|
|
return !isParentOfOthers && (stub.versionNumber && stub.versionNumber > 0 || !isOriginalOfOthers);
|
2025-09-03 17:47:58 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
// For each leaf file, build its complete lineage path back to original
|
|
|
|
for (const leafFile of leafFiles) {
|
2025-09-05 17:41:53 +01:00
|
|
|
const lineagePath: StirlingFileStub[] = [];
|
|
|
|
let currentFile: StirlingFileStub | undefined = leafFile;
|
|
|
|
|
2025-09-03 17:47:58 +01:00
|
|
|
// Trace back through parentFileId chain to build this specific branch
|
|
|
|
while (currentFile) {
|
|
|
|
lineagePath.push(currentFile);
|
2025-09-05 17:41:53 +01:00
|
|
|
|
2025-09-03 17:47:58 +01:00
|
|
|
// Move to parent file in this branch
|
2025-09-05 17:41:53 +01:00
|
|
|
let nextFile: StirlingFileStub | undefined = undefined;
|
|
|
|
|
2025-09-03 17:47:58 +01:00
|
|
|
if (currentFile.parentFileId) {
|
|
|
|
nextFile = fileMap.get(currentFile.parentFileId);
|
|
|
|
} else if (currentFile.originalFileId && currentFile.originalFileId !== currentFile.id) {
|
|
|
|
// For v1 files, the original file might be referenced by originalFileId
|
|
|
|
nextFile = fileMap.get(currentFile.originalFileId);
|
2025-09-03 14:48:14 +01:00
|
|
|
}
|
2025-09-05 17:41:53 +01:00
|
|
|
|
2025-09-03 17:47:58 +01:00
|
|
|
// Check for infinite loops before moving to next
|
|
|
|
if (nextFile && lineagePath.some(file => file.id === nextFile!.id)) {
|
|
|
|
break;
|
|
|
|
}
|
2025-09-05 17:41:53 +01:00
|
|
|
|
2025-09-03 17:47:58 +01:00
|
|
|
currentFile = nextFile;
|
2025-09-03 14:48:14 +01:00
|
|
|
}
|
2025-09-05 17:41:53 +01:00
|
|
|
|
2025-09-03 17:47:58 +01:00
|
|
|
// Sort lineage with latest version first (leaf at top)
|
|
|
|
lineagePath.sort((a, b) => (b.versionNumber || 0) - (a.versionNumber || 0));
|
2025-09-05 17:41:53 +01:00
|
|
|
|
2025-09-03 17:47:58 +01:00
|
|
|
// Use leaf file ID as the group key - each branch gets its own group
|
|
|
|
groups.set(leafFile.id, lineagePath);
|
2025-09-02 17:24:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return groups;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-09-04 11:26:55 +01:00
|
|
|
* Get the latest version of each file group (optimized version using leaf flags)
|
2025-09-02 17:24:26 +01:00
|
|
|
*/
|
2025-09-05 17:41:53 +01:00
|
|
|
export function getLatestVersions(fileStubs: StirlingFileStub[]): StirlingFileStub[] {
|
2025-09-04 11:26:55 +01:00
|
|
|
// If we have leaf flags, use them for much faster filtering
|
2025-09-05 17:41:53 +01:00
|
|
|
const hasLeafFlags = fileStubs.some(fileStub => fileStub.isLeaf !== undefined);
|
|
|
|
|
2025-09-04 11:26:55 +01:00
|
|
|
if (hasLeafFlags) {
|
|
|
|
// Fast path: just return files marked as leaf nodes
|
2025-09-05 17:41:53 +01:00
|
|
|
return fileStubs.filter(fileStub => fileStub.isLeaf !== false); // Default to true if undefined
|
2025-09-04 11:26:55 +01:00
|
|
|
} else {
|
|
|
|
// Fallback to expensive calculation for backward compatibility
|
2025-09-05 17:41:53 +01:00
|
|
|
const groups = groupFilesByOriginal(fileStubs);
|
|
|
|
const latestVersions: StirlingFileStub[] = [];
|
2025-09-04 11:26:55 +01:00
|
|
|
|
2025-09-05 17:41:53 +01:00
|
|
|
for (const [_, fileStubs] of groups) {
|
|
|
|
if (fileStubs.length > 0) {
|
2025-09-04 11:26:55 +01:00
|
|
|
// First item is the latest version (sorted desc by version number)
|
2025-09-05 17:41:53 +01:00
|
|
|
latestVersions.push(fileStubs[0]);
|
2025-09-04 11:26:55 +01:00
|
|
|
}
|
2025-09-02 17:24:26 +01:00
|
|
|
}
|
|
|
|
|
2025-09-04 11:26:55 +01:00
|
|
|
return latestVersions;
|
|
|
|
}
|
2025-09-02 17:24:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get version history for a file
|
|
|
|
*/
|
|
|
|
export function getVersionHistory(
|
2025-09-05 17:41:53 +01:00
|
|
|
targetFileStub: StirlingFileStub,
|
|
|
|
allFileStubs: StirlingFileStub[]
|
|
|
|
): StirlingFileStub[] {
|
|
|
|
const originalId = targetFileStub.originalFileId || targetFileStub.id;
|
|
|
|
|
|
|
|
return allFileStubs
|
|
|
|
.filter(fileStub => {
|
|
|
|
const fileStubOriginalId = fileStub.originalFileId || fileStub.id;
|
|
|
|
return fileStubOriginalId === originalId;
|
2025-09-02 17:24:26 +01:00
|
|
|
})
|
|
|
|
.sort((a, b) => (b.versionNumber || 0) - (a.versionNumber || 0));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a file has version history
|
|
|
|
*/
|
2025-09-05 17:41:53 +01:00
|
|
|
export function hasVersionHistory(fileStub: StirlingFileStub): boolean {
|
|
|
|
return !!(fileStub.originalFileId && fileStub.versionNumber && fileStub.versionNumber > 0);
|
2025-09-02 17:24:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate a descriptive name for a file version
|
|
|
|
*/
|
2025-09-05 17:41:53 +01:00
|
|
|
export function generateVersionName(fileStub: StirlingFileStub): string {
|
|
|
|
const baseName = fileStub.name.replace(/\.pdf$/i, '');
|
2025-09-03 14:48:14 +01:00
|
|
|
|
2025-09-05 17:41:53 +01:00
|
|
|
if (!hasVersionHistory(fileStub)) {
|
|
|
|
return fileStub.name;
|
2025-09-02 17:24:26 +01:00
|
|
|
}
|
|
|
|
|
2025-09-05 17:41:53 +01:00
|
|
|
const versionInfo = fileStub.versionNumber ? ` (v${fileStub.versionNumber})` : '';
|
|
|
|
const toolInfo = fileStub.toolHistory && fileStub.toolHistory.length > 0
|
|
|
|
? ` - ${fileStub.toolHistory[fileStub.toolHistory.length - 1].toolName}`
|
2025-09-02 17:24:26 +01:00
|
|
|
: '';
|
2025-09-03 14:48:14 +01:00
|
|
|
|
2025-09-02 17:24:26 +01:00
|
|
|
return `${baseName}${versionInfo}${toolInfo}.pdf`;
|
|
|
|
}
|
|
|
|
|
2025-09-04 11:26:55 +01:00
|
|
|
/**
|
|
|
|
* Get recent files efficiently using leaf flags from IndexedDB
|
|
|
|
* This is much faster than loading all files and calculating leaf nodes
|
|
|
|
*/
|
|
|
|
export async function getRecentLeafFiles(): Promise<import('../services/fileStorage').StoredFile[]> {
|
|
|
|
try {
|
|
|
|
const { fileStorage } = await import('../services/fileStorage');
|
|
|
|
return await fileStorage.getLeafFiles();
|
|
|
|
} catch (error) {
|
|
|
|
console.warn('Failed to get recent leaf files from IndexedDB:', error);
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get recent file metadata efficiently using leaf flags from IndexedDB
|
|
|
|
* This is much faster than loading all files and calculating leaf nodes
|
|
|
|
*/
|
2025-09-10 10:03:35 +01:00
|
|
|
export async function getRecentLeafFileMetadata(): Promise<StoredFileMetadata[]> {
|
2025-09-04 11:26:55 +01:00
|
|
|
try {
|
|
|
|
const { fileStorage } = await import('../services/fileStorage');
|
|
|
|
return await fileStorage.getLeafFileMetadata();
|
|
|
|
} catch (error) {
|
|
|
|
console.warn('Failed to get recent leaf file metadata from IndexedDB:', error);
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-09-04 12:11:09 +01:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-10 10:03:35 +01:00
|
|
|
* Create basic metadata for storing files
|
|
|
|
* History information is managed separately in IndexedDB
|
2025-09-02 17:24:26 +01:00
|
|
|
*/
|
|
|
|
export async function createFileMetadataWithHistory(
|
2025-09-03 14:48:14 +01:00
|
|
|
file: File,
|
|
|
|
fileId: FileId,
|
2025-09-02 17:24:26 +01:00
|
|
|
thumbnail?: string
|
2025-09-10 10:03:35 +01:00
|
|
|
): Promise<StoredFileMetadata> {
|
|
|
|
return {
|
2025-09-02 17:24:26 +01:00
|
|
|
id: fileId,
|
|
|
|
name: file.name,
|
|
|
|
type: file.type,
|
|
|
|
size: file.size,
|
|
|
|
lastModified: file.lastModified,
|
2025-09-04 11:26:55 +01:00
|
|
|
thumbnail,
|
|
|
|
isLeaf: true // New files are leaf nodes by default
|
2025-09-02 17:24:26 +01:00
|
|
|
};
|
2025-09-03 14:48:14 +01:00
|
|
|
}
|