mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-28 07:09:23 +00:00

# Description of Changes <!-- Please provide a summary of the changes, including: Rewrite of page editor to make it work properly. Added page breaks Added merged file support Added "insert file" support Slight Ux improvements Closes #(issue_number) --> --- ## 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>
185 lines
4.8 KiB
TypeScript
185 lines
4.8 KiB
TypeScript
/**
|
|
* PDF.js Worker Manager - Centralized worker lifecycle management
|
|
*
|
|
* Prevents infinite worker creation by managing PDF.js workers globally
|
|
* and ensuring proper cleanup when operations complete.
|
|
*/
|
|
|
|
import * as pdfjsLib from 'pdfjs-dist';
|
|
const { getDocument, GlobalWorkerOptions } = pdfjsLib;
|
|
|
|
class PDFWorkerManager {
|
|
private static instance: PDFWorkerManager;
|
|
private activeDocuments = new Set<any>();
|
|
private workerCount = 0;
|
|
private maxWorkers = 10; // Limit concurrent workers
|
|
private isInitialized = false;
|
|
|
|
private constructor() {
|
|
this.initializeWorker();
|
|
}
|
|
|
|
static getInstance(): PDFWorkerManager {
|
|
if (!PDFWorkerManager.instance) {
|
|
PDFWorkerManager.instance = new PDFWorkerManager();
|
|
}
|
|
return PDFWorkerManager.instance;
|
|
}
|
|
|
|
/**
|
|
* Initialize PDF.js worker once globally
|
|
*/
|
|
private initializeWorker(): void {
|
|
if (!this.isInitialized) {
|
|
GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
|
this.isInitialized = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a PDF document with proper lifecycle management
|
|
* Supports ArrayBuffer, Uint8Array, URL string, or {data: ArrayBuffer} object
|
|
*/
|
|
async createDocument(
|
|
data: ArrayBuffer | Uint8Array | string | { data: ArrayBuffer },
|
|
options: {
|
|
disableAutoFetch?: boolean;
|
|
disableStream?: boolean;
|
|
stopAtErrors?: boolean;
|
|
verbosity?: number;
|
|
} = {}
|
|
): Promise<any> {
|
|
// Wait if we've hit the worker limit
|
|
if (this.activeDocuments.size >= this.maxWorkers) {
|
|
await this.waitForAvailableWorker();
|
|
}
|
|
|
|
// Normalize input data to PDF.js format
|
|
let pdfData: any;
|
|
if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
|
|
pdfData = { data };
|
|
} else if (typeof data === 'string') {
|
|
pdfData = data; // URL string
|
|
} else if (data && typeof data === 'object' && 'data' in data) {
|
|
pdfData = data; // Already in {data: ArrayBuffer} format
|
|
} else {
|
|
pdfData = data; // Pass through as-is
|
|
}
|
|
|
|
const loadingTask = getDocument(
|
|
typeof pdfData === 'string' ? {
|
|
url: pdfData,
|
|
disableAutoFetch: options.disableAutoFetch ?? true,
|
|
disableStream: options.disableStream ?? true,
|
|
stopAtErrors: options.stopAtErrors ?? false,
|
|
verbosity: options.verbosity ?? 0
|
|
} : {
|
|
...pdfData,
|
|
disableAutoFetch: options.disableAutoFetch ?? true,
|
|
disableStream: options.disableStream ?? true,
|
|
stopAtErrors: options.stopAtErrors ?? false,
|
|
verbosity: options.verbosity ?? 0
|
|
}
|
|
);
|
|
|
|
try {
|
|
const pdf = await loadingTask.promise;
|
|
this.activeDocuments.add(pdf);
|
|
this.workerCount++;
|
|
|
|
return pdf;
|
|
} catch (error) {
|
|
// If document creation fails, make sure to clean up the loading task
|
|
if (loadingTask) {
|
|
try {
|
|
loadingTask.destroy();
|
|
} catch (destroyError) {
|
|
}
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Properly destroy a PDF document and clean up resources
|
|
*/
|
|
destroyDocument(pdf: any): void {
|
|
if (this.activeDocuments.has(pdf)) {
|
|
try {
|
|
pdf.destroy();
|
|
this.activeDocuments.delete(pdf);
|
|
this.workerCount = Math.max(0, this.workerCount - 1);
|
|
} catch (error) {
|
|
// Still remove from tracking even if destroy failed
|
|
this.activeDocuments.delete(pdf);
|
|
this.workerCount = Math.max(0, this.workerCount - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destroy all active PDF documents
|
|
*/
|
|
destroyAllDocuments(): void {
|
|
const documentsToDestroy = Array.from(this.activeDocuments);
|
|
documentsToDestroy.forEach(pdf => {
|
|
this.destroyDocument(pdf);
|
|
});
|
|
|
|
this.activeDocuments.clear();
|
|
this.workerCount = 0;
|
|
}
|
|
|
|
/**
|
|
* Wait for a worker to become available
|
|
*/
|
|
private async waitForAvailableWorker(): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
const checkAvailability = () => {
|
|
if (this.activeDocuments.size < this.maxWorkers) {
|
|
resolve();
|
|
} else {
|
|
setTimeout(checkAvailability, 100);
|
|
}
|
|
};
|
|
checkAvailability();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get current worker statistics
|
|
*/
|
|
getWorkerStats() {
|
|
return {
|
|
active: this.activeDocuments.size,
|
|
max: this.maxWorkers,
|
|
total: this.workerCount
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Force cleanup of all workers (emergency cleanup)
|
|
*/
|
|
emergencyCleanup(): void {
|
|
// Force destroy all documents
|
|
this.activeDocuments.forEach(pdf => {
|
|
try {
|
|
pdf.destroy();
|
|
} catch (error) {
|
|
}
|
|
});
|
|
|
|
this.activeDocuments.clear();
|
|
this.workerCount = 0;
|
|
}
|
|
|
|
/**
|
|
* Set maximum concurrent workers
|
|
*/
|
|
setMaxWorkers(max: number): void {
|
|
this.maxWorkers = Math.max(1, Math.min(max, 15)); // Between 1-15 workers for multi-file support
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const pdfWorkerManager = PDFWorkerManager.getInstance(); |