158 lines
6.4 KiB
JavaScript
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;
|
|
|
|
|