mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-21 19:59:24 +00:00

# Description of Changes <!-- File context for managing files between tools and views Optimisation for large files Updated Split to work with new file system and match Matts stepped design closer --> --- ## 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: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
282 lines
7.8 KiB
TypeScript
282 lines
7.8 KiB
TypeScript
import { ProcessingError } from '../types/processing';
|
|
|
|
export class ProcessingErrorHandler {
|
|
private static readonly DEFAULT_MAX_RETRIES = 3;
|
|
private static readonly RETRY_DELAYS = [1000, 2000, 4000]; // Progressive backoff in ms
|
|
|
|
/**
|
|
* Create a ProcessingError from an unknown error
|
|
*/
|
|
static createProcessingError(
|
|
error: unknown,
|
|
retryCount: number = 0,
|
|
maxRetries: number = this.DEFAULT_MAX_RETRIES
|
|
): ProcessingError {
|
|
const originalError = error instanceof Error ? error : new Error(String(error));
|
|
const message = originalError.message;
|
|
|
|
// Determine error type based on error message and properties
|
|
const errorType = this.determineErrorType(originalError, message);
|
|
|
|
// Determine if error is recoverable
|
|
const recoverable = this.isRecoverable(errorType, retryCount, maxRetries);
|
|
|
|
return {
|
|
type: errorType,
|
|
message: this.formatErrorMessage(errorType, message),
|
|
recoverable,
|
|
retryCount,
|
|
maxRetries,
|
|
originalError
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Determine the type of error based on error characteristics
|
|
*/
|
|
private static determineErrorType(error: Error, message: string): ProcessingError['type'] {
|
|
const lowerMessage = message.toLowerCase();
|
|
|
|
// Network-related errors
|
|
if (lowerMessage.includes('network') ||
|
|
lowerMessage.includes('fetch') ||
|
|
lowerMessage.includes('connection')) {
|
|
return 'network';
|
|
}
|
|
|
|
// Memory-related errors
|
|
if (lowerMessage.includes('memory') ||
|
|
lowerMessage.includes('quota') ||
|
|
lowerMessage.includes('allocation') ||
|
|
error.name === 'QuotaExceededError') {
|
|
return 'memory';
|
|
}
|
|
|
|
// Timeout errors
|
|
if (lowerMessage.includes('timeout') ||
|
|
lowerMessage.includes('aborted') ||
|
|
error.name === 'AbortError') {
|
|
return 'timeout';
|
|
}
|
|
|
|
// Cancellation
|
|
if (lowerMessage.includes('cancel') ||
|
|
lowerMessage.includes('abort') ||
|
|
error.name === 'AbortError') {
|
|
return 'cancelled';
|
|
}
|
|
|
|
// PDF corruption/parsing errors
|
|
if (lowerMessage.includes('pdf') ||
|
|
lowerMessage.includes('parse') ||
|
|
lowerMessage.includes('invalid') ||
|
|
lowerMessage.includes('corrupt') ||
|
|
lowerMessage.includes('malformed')) {
|
|
return 'corruption';
|
|
}
|
|
|
|
// Default to parsing error
|
|
return 'parsing';
|
|
}
|
|
|
|
/**
|
|
* Determine if an error is recoverable based on type and retry count
|
|
*/
|
|
private static isRecoverable(
|
|
errorType: ProcessingError['type'],
|
|
retryCount: number,
|
|
maxRetries: number
|
|
): boolean {
|
|
// Never recoverable
|
|
if (errorType === 'cancelled' || errorType === 'corruption') {
|
|
return false;
|
|
}
|
|
|
|
// Recoverable if we haven't exceeded retry count
|
|
if (retryCount >= maxRetries) {
|
|
return false;
|
|
}
|
|
|
|
// Memory errors are usually not recoverable
|
|
if (errorType === 'memory') {
|
|
return retryCount < 1; // Only one retry for memory errors
|
|
}
|
|
|
|
// Network and timeout errors are usually recoverable
|
|
return errorType === 'network' || errorType === 'timeout' || errorType === 'parsing';
|
|
}
|
|
|
|
/**
|
|
* Format error message for user display
|
|
*/
|
|
private static formatErrorMessage(errorType: ProcessingError['type'], originalMessage: string): string {
|
|
switch (errorType) {
|
|
case 'network':
|
|
return 'Network connection failed. Please check your internet connection and try again.';
|
|
|
|
case 'memory':
|
|
return 'Insufficient memory to process this file. Try closing other applications or processing a smaller file.';
|
|
|
|
case 'timeout':
|
|
return 'Processing timed out. This file may be too large or complex to process.';
|
|
|
|
case 'cancelled':
|
|
return 'Processing was cancelled by user.';
|
|
|
|
case 'corruption':
|
|
return 'This PDF file appears to be corrupted or encrypted. Please try a different file.';
|
|
|
|
case 'parsing':
|
|
return `Failed to process PDF: ${originalMessage}`;
|
|
|
|
default:
|
|
return `Processing failed: ${originalMessage}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute an operation with automatic retry logic
|
|
*/
|
|
static async executeWithRetry<T>(
|
|
operation: () => Promise<T>,
|
|
onError?: (error: ProcessingError) => void,
|
|
maxRetries: number = this.DEFAULT_MAX_RETRIES
|
|
): Promise<T> {
|
|
let lastError: ProcessingError | null = null;
|
|
|
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
try {
|
|
return await operation();
|
|
} catch (error) {
|
|
lastError = this.createProcessingError(error, attempt, maxRetries);
|
|
|
|
// Notify error handler
|
|
if (onError) {
|
|
onError(lastError);
|
|
}
|
|
|
|
// Don't retry if not recoverable
|
|
if (!lastError.recoverable) {
|
|
break;
|
|
}
|
|
|
|
// Don't retry on last attempt
|
|
if (attempt === maxRetries) {
|
|
break;
|
|
}
|
|
|
|
// Wait before retry with progressive backoff
|
|
const delay = this.RETRY_DELAYS[Math.min(attempt, this.RETRY_DELAYS.length - 1)];
|
|
await this.delay(delay);
|
|
|
|
console.log(`Retrying operation (attempt ${attempt + 2}/${maxRetries + 1}) after ${delay}ms delay`);
|
|
}
|
|
}
|
|
|
|
// All retries exhausted
|
|
throw lastError || new Error('Operation failed after all retries');
|
|
}
|
|
|
|
/**
|
|
* Create a timeout wrapper for operations
|
|
*/
|
|
static withTimeout<T>(
|
|
operation: () => Promise<T>,
|
|
timeoutMs: number,
|
|
timeoutMessage: string = 'Operation timed out'
|
|
): Promise<T> {
|
|
return new Promise<T>((resolve, reject) => {
|
|
const timeoutId = setTimeout(() => {
|
|
reject(new Error(timeoutMessage));
|
|
}, timeoutMs);
|
|
|
|
operation()
|
|
.then(result => {
|
|
clearTimeout(timeoutId);
|
|
resolve(result);
|
|
})
|
|
.catch(error => {
|
|
clearTimeout(timeoutId);
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create an AbortController that times out after specified duration
|
|
*/
|
|
static createTimeoutController(timeoutMs: number): AbortController {
|
|
const controller = new AbortController();
|
|
|
|
setTimeout(() => {
|
|
controller.abort();
|
|
}, timeoutMs);
|
|
|
|
return controller;
|
|
}
|
|
|
|
/**
|
|
* Check if an error indicates the operation should be retried
|
|
*/
|
|
static shouldRetry(error: ProcessingError): boolean {
|
|
return error.recoverable && error.retryCount < error.maxRetries;
|
|
}
|
|
|
|
/**
|
|
* Get user-friendly suggestions based on error type
|
|
*/
|
|
static getErrorSuggestions(error: ProcessingError): string[] {
|
|
switch (error.type) {
|
|
case 'network':
|
|
return [
|
|
'Check your internet connection',
|
|
'Try refreshing the page',
|
|
'Try again in a few moments'
|
|
];
|
|
|
|
case 'memory':
|
|
return [
|
|
'Close other browser tabs or applications',
|
|
'Try processing a smaller file',
|
|
'Restart your browser',
|
|
'Use a device with more memory'
|
|
];
|
|
|
|
case 'timeout':
|
|
return [
|
|
'Try processing a smaller file',
|
|
'Break large files into smaller sections',
|
|
'Check your internet connection speed'
|
|
];
|
|
|
|
case 'corruption':
|
|
return [
|
|
'Verify the PDF file opens in other applications',
|
|
'Try re-downloading the file',
|
|
'Try a different PDF file',
|
|
'Contact the file creator if it appears corrupted'
|
|
];
|
|
|
|
case 'parsing':
|
|
return [
|
|
'Verify this is a valid PDF file',
|
|
'Try a different PDF file',
|
|
'Contact support if the problem persists'
|
|
];
|
|
|
|
default:
|
|
return [
|
|
'Try refreshing the page',
|
|
'Try again in a few moments',
|
|
'Contact support if the problem persists'
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Utility function for delays
|
|
*/
|
|
private static delay(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
} |