UNPKG

safeer-pdf-generator

Version:

Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery

248 lines 10.9 kB
import { ChunkProcessingError, PdfGenerationError } from '../core/errors.js'; import { noOpLogger } from '../config/defaults.js'; import { Timer } from '../utils/timing.js'; import { convertImageToBase64 } from '../utils/image.js'; /** * Coordinates the processing of data chunks into PDF buffers */ export class ChunkCoordinator { constructor(logger = noOpLogger, hooks = {}) { this.logger = logger; this.hooks = hooks; } /** * Process data into chunks and generate PDF buffers */ async processChunks(options, templateCompiler, pdfEngine) { const timer = new Timer(); const { data, chunking = { enabled: true, chunkSize: 100, maxConcurrency: 2 } } = options; // If chunking is disabled or data is small, process as single chunk if (!chunking.enabled || data.length <= (chunking.chunkSize || 100)) { this.logger.info('Processing data as single chunk'); const buffer = await this.processSingleChunk(options, templateCompiler, pdfEngine); return [buffer]; } const chunkSize = chunking.chunkSize || 100; const maxConcurrency = chunking.maxConcurrency || 2; const totalChunks = Math.ceil(data.length / chunkSize); this.logger.info(`Processing ${data.length} rows in ${totalChunks} chunks ` + `(size: ${chunkSize}, concurrency: ${maxConcurrency})`); // Prepare enhanced options with logo processing const enhancedOptions = await this.prepareChunkOptions(options); // Execute before merge hook if (this.hooks.beforeMerge) { timer.mark('before-merge-hook'); await this.hooks.beforeMerge([]); timer.mark('before-merge-hook-end'); } const chunks = []; // Generate HTML for all chunks first for (let i = 0; i < totalChunks; i++) { const startIdx = i * chunkSize; const endIdx = Math.min(startIdx + chunkSize, data.length); const chunkData = data.slice(startIdx, endIdx); const chunkOptions = { ...enhancedOptions, data: chunkData, chunkInfo: { current: i + 1, total: totalChunks, startRow: startIdx + 1, endRow: endIdx, }, }; // Execute before chunk render hook if (this.hooks.beforeChunkRender) { await this.hooks.beforeChunkRender(chunkOptions); } try { const { html, header, footer } = templateCompiler(chunkOptions); // Apply HTML transform hook if available let processedHtml = html; if (this.hooks.transformHtml) { processedHtml = this.hooks.transformHtml(html, 'pre'); } chunks.push({ html: processedHtml, header, footer }); this.logger.debug(`Generated HTML for chunk ${i + 1}/${totalChunks} (rows ${startIdx + 1}-${endIdx})`); } catch (error) { throw new ChunkProcessingError(`Failed to generate HTML for chunk ${i + 1}`, i, error); } } this.logger.info(`Generated HTML for all ${totalChunks} chunks`); timer.mark('html-generation-complete'); // Generate PDFs from HTML chunks try { const pdfBuffers = await pdfEngine.generateChunks(chunks, maxConcurrency); // Execute after chunk render hooks if (this.hooks.afterChunkRender) { for (let i = 0; i < pdfBuffers.length; i++) { await this.hooks.afterChunkRender(pdfBuffers[i], { chunkIndex: i, totalChunks, }); } } const totalTime = timer.elapsed(); this.logger.info(`Completed chunk processing: ${totalChunks} chunks, ` + `${totalTime.toFixed(1)}ms total`); return pdfBuffers; } catch (error) { throw new PdfGenerationError(`Chunk processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'chunk-processing', error); } } /** * Process data as a single chunk */ async processSingleChunk(options, templateCompiler, pdfEngine) { const timer = new Timer(); const enhancedOptions = await this.prepareChunkOptions(options); // Execute before chunk render hook if (this.hooks.beforeChunkRender) { await this.hooks.beforeChunkRender(enhancedOptions); } try { const { html, header, footer } = templateCompiler(enhancedOptions); // Apply HTML transform hook if available let processedHtml = html; if (this.hooks.transformHtml) { processedHtml = this.hooks.transformHtml(html, 'pre'); processedHtml = this.hooks.transformHtml(processedHtml, 'post'); } timer.mark('html-generated'); const pdfBuffer = await pdfEngine.generatePdf(processedHtml, header, footer); // Execute after chunk render hook if (this.hooks.afterChunkRender) { await this.hooks.afterChunkRender(pdfBuffer, { chunkIndex: 0, totalChunks: 1 }); } const totalTime = timer.elapsed(); this.logger.info(`Single chunk processing completed in ${totalTime.toFixed(1)}ms`); return pdfBuffer; } catch (error) { throw new ChunkProcessingError(`Failed to process single chunk: ${error instanceof Error ? error.message : 'Unknown error'}`, 0, error); } } /** * Prepare options with enhanced features like logo processing */ async prepareChunkOptions(options) { const enhancedOptions = { ...options }; // Process company logo if present if (options.userInfo?.companyLogo) { try { this.logger.debug('Converting company logo to base64...'); const logoFetcher = options.logoFetch?.fetcher; const companyLogoBase64 = await convertImageToBase64(options.userInfo.companyLogo, this.logger, logoFetcher); enhancedOptions.userInfo = { ...options.userInfo, companyLogoBase64, }; this.logger.debug('Company logo processed successfully'); } catch (error) { this.logger.warn(`Failed to process company logo: ${error}`); // Continue without logo rather than failing enhancedOptions.userInfo = { ...options.userInfo, companyLogoBase64: '', }; } } // Apply column value transformations if hook is provided if (this.hooks.transformColumnValue && enhancedOptions.data.length > 0) { enhancedOptions.data = enhancedOptions.data.map((row, rowIndex) => { const transformedRow = { ...row }; for (const column of enhancedOptions.columns) { if (transformedRow.hasOwnProperty(column.dataIndex)) { try { const originalValue = transformedRow[column.dataIndex]; const transformedValue = this.hooks.transformColumnValue(originalValue, column, row); transformedRow[column.dataIndex] = transformedValue; } catch (error) { this.logger.warn(`Failed to transform column ${column.dataIndex} for row ${rowIndex}: ${error}`); // Keep original value on transformation error } } } return transformedRow; }); } return enhancedOptions; } /** * Calculate optimal chunk configuration based on data size and system resources */ static calculateOptimalChunking(dataLength, availableMemoryMB) { // Disable chunking for small datasets if (dataLength <= 50) { return { chunkSize: dataLength, maxConcurrency: 1, enabled: false }; } // Base calculations let chunkSize = 100; let maxConcurrency = 2; // Adjust based on data size if (dataLength > 10000) { chunkSize = 200; maxConcurrency = 3; } else if (dataLength > 5000) { chunkSize = 150; maxConcurrency = 3; } else if (dataLength > 1000) { chunkSize = 100; maxConcurrency = 2; } // Adjust based on available memory if provided if (availableMemoryMB) { if (availableMemoryMB < 512) { chunkSize = Math.min(chunkSize, 50); maxConcurrency = 1; } else if (availableMemoryMB > 2048) { maxConcurrency = Math.min(maxConcurrency + 1, 5); } } return { chunkSize, maxConcurrency, enabled: true }; } /** * Estimate memory usage for chunk processing */ static estimateMemoryUsage(_dataLength, chunkSize, maxConcurrency, avgRowSizeKB = 1) { const chunksInMemory = maxConcurrency; const rowsPerChunk = chunkSize; const estimatedChunkSizeMB = (rowsPerChunk * avgRowSizeKB) / 1024; const estimatedMB = chunksInMemory * estimatedChunkSizeMB * 3; // 3x multiplier for HTML + PDF + overhead let recommendation = 'Configuration looks good'; if (estimatedMB > 1024) { recommendation = 'High memory usage expected, consider reducing chunk size or concurrency'; } else if (estimatedMB < 50) { recommendation = 'Low memory usage, you could increase concurrency for better performance'; } return { estimatedMB, recommendation }; } /** * Validate chunk configuration */ static validateChunkConfig(config) { const errors = []; const { chunkSize = 100, maxConcurrency = 2, dataLength } = config; if (chunkSize < 1) { errors.push('Chunk size must be at least 1'); } if (chunkSize > dataLength) { errors.push('Chunk size cannot be larger than data length'); } if (maxConcurrency < 1) { errors.push('Max concurrency must be at least 1'); } if (maxConcurrency > 10) { errors.push('Max concurrency should not exceed 10 for stability'); } return { valid: errors.length === 0, errors }; } } //# sourceMappingURL=ChunkCoordinator.js.map