/** * Thermal Printer - Simplified API * Single entry point for all printing operations */ import {NativePrinter} from '../../native' import {compileDocument, Node} from '../compiler' import {cleanupImageFile} from '../shared' import {normalizeError, PrintErrors} from '../shared/errors' import type { ImageOptions, JobOptions, MultiPrinterResult, PrinterConfig, PrinterOptions, PrinterResult, PrintJob, PrintResult, } from './types' // ============= CORE FUNCTION ============= /** * Core print function - ALL printing goes through this * Handles both regular nodes and image nodes * * IMPORTANT: Maintains EXACT document order! * Example: [text1, text2, image1, text3, image2, text4] * Will print as: text1+text2 → image1 → text3 → image2 → text4 * * JS Layer responsibilities: * 1. Orchestration - control the sequence of printing * 2. Configuration - prepare image settings based on paper width * 3. Flow control - manage keepAlive flags for connection * 4. Error handling - normalize and propagate errors * * Native Layer responsibilities: * 1. Image processing - decode, resize, dither * 2. Rasterization - convert to ESC/POS bitmap format * 3. Performance - handle heavy processing * * @param cleanupImages - If true, cleanup temp image files after printing (default: true) */ async function printCore( address: string, nodes: Node[], options: PrinterOptions = {}, cleanupImages: boolean = true, ): Promise { console.log( '[JS] Printer.printCore - START: address=' + address + ', nodes=' + nodes.length + ', types=[' + nodes.map((n) => n.type).join(',') + ']', ) // Apply defaults const printerOptions: PrinterOptions = { ...options, marginMm: options.marginMm !== undefined ? options.marginMm : 0, } // Check if document contains image nodes const hasImages = nodes.some((n) => n.type === 'image') if (hasImages) { console.log('[JS] printCore: Mixed content detected') let currentBatch: Node[] = [] const totalNodes = nodes.length for (let i = 0; i < totalNodes; i++) { const node = nodes[i] console.log('[JS] printCore: Processing node ' + (i + 1) + '/' + totalNodes + ' type=' + node.type) if (node.type !== 'image') { currentBatch.push(node) continue } // Flush non-image nodes before the image if (currentBatch.length > 0) { const data = Array.from(compileDocument(currentBatch, printerOptions)) const res = await NativePrinter.printRaw(address, data, {keepAlive: true, timeout: 10000}) if (!res.success) throw PrintErrors.fromNativeResult(res) currentBatch = [] } // Print image node const remainingCount = totalNodes - i - 1 const imageOptions: ImageOptions = { ...node.options, // Inherit global margin if not set in node options marginMm: node.options?.marginMm !== undefined ? node.options.marginMm : printerOptions.marginMm, keepAlive: remainingCount > 0 || !!options.keepAlive, } const imgRes = await NativePrinter.printImage(address, node.imagePath, imageOptions, printerOptions) if (!imgRes.success) throw PrintErrors.fromNativeResult(imgRes) // Cleanup temp file after successful print (only if cleanupImages is true) if (cleanupImages) { await cleanupImageFile(node.imagePath) } } // Flush trailing non-image nodes after last image if (currentBatch.length > 0) { const data = Array.from(compileDocument(currentBatch, printerOptions)) const res = await NativePrinter.printRaw(address, data, {keepAlive: options.keepAlive, timeout: 10000}) if (!res.success) throw PrintErrors.fromNativeResult(res) } } else { // No images: compile all nodes and print in one go const data = Array.from(compileDocument(nodes, printerOptions)) console.log('[JS] printCore: Sending ' + data.length + ' bytes to printer') const result = await NativePrinter.printRaw(address, data, { keepAlive: options.keepAlive, timeout: 10000, }) if (!result.success) { console.error('[JS] printCore ERROR: ' + JSON.stringify(result.error)) throw PrintErrors.fromNativeResult(result) } } console.log('[JS] printCore SUCCESS') } // ============= HELPER FUNCTIONS ============= /** * Print with copies - handles multiple copies of same document */ async function printWithCopies( address: string, nodes: Node[], copies: number, delayMs: number, options: PrinterOptions, ): Promise { for (let i = 0; i < copies; i++) { console.log('[JS] printWithCopies: Copy ' + (i + 1) + '/' + copies) // Keep alive between copies (except last) const keepAlive = i < copies - 1 || options.keepAlive // Only cleanup images on the last copy to avoid deleting files needed for subsequent copies const cleanupImages = i === copies - 1 await printCore(address, nodes, {...options, keepAlive}, cleanupImages) // Delay between copies if (i < copies - 1 && delayMs > 0) { await new Promise((resolve) => setTimeout(resolve, delayMs)) } } } // ============= HANDLER FUNCTIONS ============= /** * Handle single printer, single document */ async function handleSinglePrinterSingleDoc( printer: PrinterConfig, document: Node[], ): Promise { const startTime = Date.now() const results = new Map() try { const copies = printer.copies || 1 if (copies > 1) { await printWithCopies( printer.address, document, copies, printer.delayBetweenCopies || 200, printer.options || {}, ) } else { // Single copy: cleanup images after printing await printCore(printer.address, document, printer.options, true) } const duration = Date.now() - startTime results.set(printer.address, {success: true, duration}) return {success: true, results, totalDuration: duration} } catch (error) { const printError = normalizeError(error) console.error( '[JS] Printer.handleSinglePrinterSingleDoc - ERROR: code=' + printError.code + ', msg=' + printError.message, ) const duration = Date.now() - startTime results.set(printer.address, {success: false, error: printError, duration}) return {success: false, results, totalDuration: duration} } } /** * Handle single printer, multiple documents */ async function handleSinglePrinterMultipleDocs( printer: PrinterConfig, documents: Node[][], options?: JobOptions, ): Promise { const startTime = Date.now() const results = new Map() try { const copies = printer.copies || 1 let completed = 0 const total = documents.length * copies for (let i = 0; i < documents.length; i++) { console.log('[JS] handleSinglePrinterMultipleDocs: Document ' + (i + 1) + '/' + documents.length) // Use printWithCopies helper for consistency const isLastDocument = i === documents.length - 1 const optionsWithKeepAlive = { ...printer.options, keepAlive: !isLastDocument || printer.options?.keepAlive, } await printWithCopies( printer.address, documents[i], copies, printer.delayBetweenCopies || 200, optionsWithKeepAlive, ) completed += copies options?.onProgress?.(completed, total) } const duration = Date.now() - startTime results.set(printer.address, {success: true, duration}) return {success: true, results, totalDuration: duration} } catch (error) { const printError = normalizeError(error) console.error( '[JS] Printer.handleSinglePrinterMultipleDocs - ERROR: code=' + printError.code + ', msg=' + printError.message, ) const duration = Date.now() - startTime results.set(printer.address, {success: false, error: printError, duration}) return {success: false, results, totalDuration: duration} } } /** * Handle multiple printers, single document */ async function handleMultiplePrintersSingleDoc( printers: PrinterConfig[], document: Node[], options?: JobOptions, ): Promise { const startTime = Date.now() const results = new Map() let completed = 0 const total = printers.reduce((sum, p) => sum + (p.copies || 1), 0) const printToPrinter = async (printer: PrinterConfig) => { const printerStart = Date.now() try { const copies = printer.copies || 1 await printWithCopies( printer.address, document, copies, printer.delayBetweenCopies || 200, printer.options || {}, ) const duration = Date.now() - printerStart results.set(printer.address, {success: true, duration}) completed += copies options?.onProgress?.(completed, total) options?.onJobComplete?.(printer.address, true) } catch (error) { const printError = normalizeError(error) const duration = Date.now() - printerStart results.set(printer.address, { success: false, error: printError, duration, }) completed += printer.copies || 1 options?.onProgress?.(completed, total) options?.onJobComplete?.(printer.address, false, printError) if (!options?.continueOnError) { throw printError } } } try { if (options?.concurrent) { // Parallel execution await Promise.all(printers.map(printToPrinter)) } else { // Sequential execution for (const printer of printers) { await printToPrinter(printer) } } } catch (error) { console.error('[JS] handleMultiplePrintersSingleDoc STOPPED:', error) } const totalDuration = Date.now() - startTime const success = Array.from(results.values()).every((r) => r.success) return {success, results, totalDuration} } /** * Handle multiple printers, multiple documents * Each printer prints all documents */ async function handleMultiplePrintersMultipleDocs( printers: PrinterConfig[], documents: Node[][], options?: JobOptions, ): Promise { const startTime = Date.now() const results = new Map() let completed = 0 const total = printers.reduce((sum, p) => sum + (p.copies || 1) * documents.length, 0) const printToPrinter = async (printer: PrinterConfig) => { const printerStart = Date.now() try { const copies = printer.copies || 1 // Print all documents to this printer for (let i = 0; i < documents.length; i++) { console.log( '[JS] Printer.handleMultiplePrintersMultipleDocs - PROCESS: [' + printer.address + '] Document ' + (i + 1) + '/' + documents.length, ) // Use printWithCopies helper for consistency const isLastDocument = i === documents.length - 1 const optionsWithKeepAlive = { ...printer.options, keepAlive: !isLastDocument || printer.options?.keepAlive, } await printWithCopies( printer.address, documents[i], copies, printer.delayBetweenCopies || 200, optionsWithKeepAlive, ) completed += copies options?.onProgress?.(completed, total) } const duration = Date.now() - printerStart results.set(printer.address, {success: true, duration}) options?.onJobComplete?.(printer.address, true) } catch (error) { const duration = Date.now() - printerStart results.set(printer.address, { success: false, error: normalizeError(error), duration, }) const printError = normalizeError(error) options?.onJobComplete?.(printer.address, false, printError) if (!options?.continueOnError) { throw printError } } } try { if (options?.concurrent) { await Promise.all(printers.map(printToPrinter)) } else { for (const printer of printers) { await printToPrinter(printer) } } } catch (error) { console.error('[JS] handleMultiplePrintersMultipleDocs STOPPED:', error) } const totalDuration = Date.now() - startTime const success = Array.from(results.values()).every((r) => r.success) return {success, results, totalDuration} } // ============= MAIN API FUNCTION ============= /** * Main print function - single entry point for all printing * Routes to appropriate handler based on printers and documents count * Always returns MultiPrinterResult for consistent result parsing */ export async function printReceipt(job: PrintJob): Promise { const {printers, documents, options} = job // Validation if (!printers || printers.length === 0) { throw PrintErrors.noPrinters() } if (!documents || documents.length === 0) { throw PrintErrors.noDocuments() } // Apply defaults to each printer printers.forEach((printer) => { printer.copies = printer.copies || 1 printer.delayBetweenCopies = printer.delayBetweenCopies || 200 printer.options = printer.options || {} }) // Route to appropriate handler if (printers.length === 1 && documents.length === 1) { // Case 1: Single printer, single document console.log('[JS] printReceipt: Single printer, single document') return handleSinglePrinterSingleDoc(printers[0], documents[0]) } if (printers.length === 1 && documents.length > 1) { // Case 2: Single printer, multiple documents console.log('[JS] printReceipt: Single printer, ' + documents.length + ' documents') return handleSinglePrinterMultipleDocs(printers[0], documents, options) } if (printers.length > 1 && documents.length === 1) { // Case 3: Multiple printers, single document console.log('[JS] printReceipt: ' + printers.length + ' printers, single document') return handleMultiplePrintersSingleDoc(printers, documents[0], options) } if (printers.length > 1 && documents.length > 1) { // Case 4: Multiple printers, multiple documents console.log('[JS] printReceipt: ' + printers.length + ' printers, ' + documents.length + ' documents') return handleMultiplePrintersMultipleDocs(printers, documents, options) } // Should never reach here throw PrintErrors.invalidConfig('Unexpected printer/document configuration') }