const config = require('./config'); const database = require('./database'); const printer = require('./printer'); class PrintQueueWorker { constructor(db = database, cfg = config, printerManager = printer) { this.db = db; this.cfg = cfg; this.printer = printerManager; this.intervalId = null; this.isRunning = false; this.isProcessing = false; } start(intervalMs = 10000) { if (this.isRunning) return; this.isRunning = true; const configured = parseInt(this.cfg.get('printWorkerInterval'), 10); const tickMs = Number.isFinite(configured) && configured > 0 ? configured : intervalMs; console.log(`[PrintQueueWorker] Starting with interval ${tickMs} ms`); this._schedule(tickMs); } stop() { this.isRunning = false; if (this.intervalId) { clearTimeout(this.intervalId); this.intervalId = null; } console.log('[PrintQueueWorker] Stopped'); } _schedule(intervalMs) { if (!this.isRunning) return; this.intervalId = setTimeout(async () => { try { await this._tick(); } catch (err) { console.error('[PrintQueueWorker] Tick error:', err.message); } finally { this._schedule(intervalMs); } }, intervalMs); } async _tick() { if (this.isProcessing) { return; // avoid overlapping runs } this.isProcessing = true; try { // Skip if no printers likely reachable to avoid hammering try { const printerConfigs = this.db.getEnabledPrinters(); const anyReachable = await this.printer.anyConfiguredPrinterReachable(printerConfigs); if (!anyReachable) { console.log('[PrintQueueWorker] No reachable printers detected yet, will retry later'); return; } } catch (_) { // If reachability check fails, proceed and let print attempt decide } // Recover stuck processing jobs older than 2 minutes try { this.db.resetStuckProcessingJobs(120); } catch (_) {} const pending = this.db.getPendingPrintJobs(); if (!pending || pending.length === 0) { return; } console.log(`[PrintQueueWorker] Processing ${pending.length} pending print job(s)`); for (const job of pending) { try { const order = this.db.getOrderById(job.order_id); if (!order) { console.warn(`[PrintQueueWorker] Order ${job.order_id} not found, marking job ${job.id} failed`); this.db.markPrintJobFailed(job.id); continue; } // Check 1: If the order has been printed after the job was created, mark job completed to avoid duplicates if (order.printedAt && job.created_at && Number(order.printedAt) >= Number(job.created_at)) { console.log(`[PrintQueueWorker] Skipping job ${job.id}, order ${order.id} already printed at ${order.printedAt}`); this.db.markPrintJobCompleted(job.id); continue; } // Check 2: Verify there's no active job (pending/processing) for this order+type (within last 60 seconds) // Do NOT block reprints because of a recently COMPLETED job; users may intentionally reprint. const activeCheck = this.db.hasActiveOrRecentJob(job.order_id, job.print_type, 60); if (activeCheck.hasActive && activeCheck.jobId !== job.id) { // Skip if another active pending/processing job exists if (activeCheck.status === 'pending' || activeCheck.status === 'processing') { console.log(`[PrintQueueWorker] Skipping job ${job.id}, another active job ${activeCheck.jobId} (${activeCheck.status}) exists for order ${job.order_id} (${job.print_type})`); this.db.markPrintJobCompleted(job.id); continue; } // If the recent job is COMPLETED, only block for non-reprint types (avoid duplicate cancellations/new) if (activeCheck.status === 'completed' && job.print_type !== 'reprint') { console.log(`[PrintQueueWorker] Skipping job ${job.id}, recent completed job ${activeCheck.jobId} exists for order ${job.order_id} (${job.print_type})`); this.db.markPrintJobCompleted(job.id); continue; } } // Check 3: For cancellations, verify we haven't already printed one if (job.print_type === 'canceled' && this.db.hasPrintedCancellation(job.order_id)) { console.log(`[PrintQueueWorker] Skipping job ${job.id}, cancellation already printed for order ${job.order_id}`); this.db.markPrintJobCompleted(job.id); continue; } let type = job.print_type; if (type === 'reprint') { type = order.localStatus === 'canceled' ? 'canceled' : 'new'; } const printerConfigs = this.db.getEnabledPrinters(); const cancelReason = (order && (order.cancellationReason || (order.order && order.order.cancellationReason))) || 'Order canceled'; const options = type === 'canceled' ? { reason: cancelReason } : {}; let result; if (printerConfigs && printerConfigs.length > 0) { result = await this.printer.printOrderReceiptWithPrinterConfigs(order, printerConfigs, type, options); } else { const appConfig = this.cfg.getAll(); if (!this.printer.printer || !this.printer.config) { try { this.printer.initializePrinter(appConfig); } catch (_) {} } result = await this.printer.printOrderReceipt(order, type, options); } if (result && result.success) { this.db.markOrderPrinted(order.id); this.db.markPrintJobCompleted(job.id); // Cleanup any other pending/processing jobs for this order+type to prevent duplicates this.db.cleanupDuplicateJobs(job.id, job.order_id, job.print_type); console.log(`[PrintQueueWorker] ✓ Printed order #${order.id} (job ${job.id})`); } else { // Keep as pending for retry on next tick const errMsg = result && result.error ? result.error : 'Unknown print error'; console.warn(`[PrintQueueWorker] ✗ Print failed for order #${order.id} (job ${job.id}): ${errMsg}`); } } catch (errJob) { console.error(`[PrintQueueWorker] Job ${job.id} error:`, errJob.message); } } } finally { this.isProcessing = false; } } } module.exports = PrintQueueWorker;