Files
kitchen-agent/print-worker.js
2025-10-23 19:02:56 -04:00

158 lines
6.4 KiB
JavaScript

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;