From 61194229078bea16b55e80cb11c2d4c68be79e39 Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Fri, 16 Oct 2026 18:48:29 +0530 Subject: [PATCH] Send ready receipts without optional initialization delay --- .../static/script/js/core/receipt-designer.js | 2 +- src/windows-print-queue.js | 21 ++++++------ tests/receipt-designer.test.js | 11 +++++++ tests/tools/receipt-cold-paint-proof.cjs | 3 +- tests/windows-print-queue.test.js | 33 +++++++++++-------- 5 files changed, 43 insertions(+), 27 deletions(-) diff --git a/frontend/static/script/js/core/receipt-designer.js b/frontend/static/script/js/core/receipt-designer.js index 561c2e31c..22ed87f83 100644 --- a/frontend/static/script/js/core/receipt-designer.js +++ b/frontend/static/script/js/core/receipt-designer.js @@ -415,7 +415,7 @@ if (boundary) text += '\n'; } walk(receipt); - text = text.replace(/₹/g, 'Rs. ').replace(/×/g, 'x').replace(/−/g, '-').replace(/…/g, '...').replace(/\u00a0/g, ' ').replace(/\u00b7/g, '-'); + text = text.replace(/\u00a3/g, 'GBP ').replace(/\u20ac/g, 'EUR ').replace(/₹/g, 'Rs. ').replace(/×/g, 'x').replace(/−/g, '-').replace(/…/g, '...').replace(/\u00a0/g, ' ').replace(/\u00b7/g, '-'); if (/[^\x09\x0a\x0d\x20-\x7e]/.test(text)) return null; var lines = text.split(/\r?\n/).map(function (line) { return line.replace(/\s+/g, ' ').trim().replace(/ \|$/, ''); }).filter(Boolean); return lines.length ? lines : null; diff --git a/src/windows-print-queue.js b/src/windows-print-queue.js index e93ef3e82..e4234eb01 100644 --- a/src/windows-print-queue.js +++ b/src/windows-print-queue.js @@ -262,13 +262,13 @@ class WindowsPrintQueue { if (job.accepted && this.live.has(job.id) && !job.lastJobError && !reason) return this.transition(job, 'sent'); return this.transition(job, 'failed', 'Print outcome unknown; check the printer before requesting a duplicate'); } - // An idle initialization must finish before a receipt is submitted. + // Optional idle traffic must not hold a ready printer for 30 seconds. + // The same per-printer writer and Windows queue preserve submission order. + // Keep failed/offline jobs blocked by readiness; suppress further idle writes + // when an old pulse is still pending rather than adding more reset jobs. const pulse = this.keepAlive[job.printer.toLowerCase()]; if (pulse?.pending && this.pulsePending(pulse, health)) { - if (this.now() - pulse.at < 30000) return this.transition(job, 'waiting', pulse.status); - // Do not bypass an actual failed Windows job: readiness still applies. - // Optional ESC @ traffic alone cannot permanently starve real receipts. - pulse.pending = false; pulse.suppressed = true; + pulse.suppressed = true; this.write('keep-alive.json', this.keepAlive); } if (job.state === 'failed') { @@ -276,7 +276,10 @@ class WindowsPrintQueue { job.reconnect = false; job.retries = 0; job.nextAt = 0; } if (job.nextAt > this.now()) return; - const canWake = !reason || (health.present === true && reason === 'Printer offline'); + // Ready printers receive the receipt immediately: enqueue already prefixes ESC @. + // A separate wake job can remain visible in Windows for 30 seconds. + // Only an actually offline device needs the recovery initialization. + const canWake = health.present === true && reason === 'Printer offline'; if (canWake && job.binding.initialize !== false && this.transport.initialize) { if (!job.wakeDocument) { job.wakeDocument = 'Posnic-initialize-' + job.id; @@ -300,11 +303,7 @@ class WindowsPrintQueue { reason = readiness(health, job.binding); if (reason) return this.defer(job, reason); } - if (health.jobs.some(item => item.document === job.wakeDocument)) { - if (job.wakeStartedAt == null) { job.wakeStartedAt = this.now(); this.write(job.id + '.json', job); } - if (this.now() - job.wakeStartedAt < 30000) return this.transition(job, 'waiting', 'Waiting for printer initialization'); - // Receipt already contains ESC @ in the same job; no new wake is sent. - } + } if (reason) return this.defer(job, reason, /offline|disconnected|Printer error|failed job|USB discovery failed|could not be identified|Multiple connected USB|Saved USB device is absent|Printer status unknown/i.test(reason)); // Persist BEFORE crossing the spooler boundary. A crash in the following diff --git a/tests/receipt-designer.test.js b/tests/receipt-designer.test.js index 5064d2071..083832c22 100644 --- a/tests/receipt-designer.test.js +++ b/tests/receipt-designer.test.js @@ -1502,3 +1502,14 @@ test('hiding payment method removes cash and card without hiding totals or balan } dom.window.close(); }); + +test('GBP and EUR receipts stay on Fast receipt without raster fallback', () => { + const {dom, engine} = setup(); + try { + const lines = engine.fastLines('

Total \u00a31.50

Cash \u00a32.00

Change \u00a30.50

Total \u20ac3.00

'); + assert.ok(lines); + const bytes = require('../src/receipt-fast').renderFastReceipt(lines, '80').toString('ascii'); + assert.match(bytes, /Total GBP 1.50/); assert.match(bytes, /Cash GBP 2.00/); + assert.match(bytes, /Change GBP 0.50/); assert.match(bytes, /Total EUR 3.00/); + } finally { dom.window.close(); } +}); diff --git a/tests/tools/receipt-cold-paint-proof.cjs b/tests/tools/receipt-cold-paint-proof.cjs index 739184d9e..9b0297630 100644 --- a/tests/tools/receipt-cold-paint-proof.cjs +++ b/tests/tools/receipt-cold-paint-proof.cjs @@ -15,6 +15,7 @@ app.whenReady().then(async () => { const body = Array.from({ length: 12 }, (_, i) => `
`).join(''); const document = `
Synthetic receipt total${body}
`; for (let attempt = 0; attempt < 3; attempt++) { + const started = performance.now(); const strips = await rasterize({ document, paper, width }); assert.equal(strips.length, 12); for (let index = 0; index < strips.length; index++) { @@ -23,7 +24,7 @@ app.whenReady().then(async () => { for (let x = 0; x < width / 8; x++) assert.equal(data[row * width / 8 + x], x < (index + 1) * 2 ? 255 : 0, `paper=${paper},attempt=${attempt},strip=${index},row=${row},byte=${x}`); } } - results.push({ paper, attempt, strips: strips.length }); + results.push({ paper, attempt, strips: strips.length, preparationMs: Math.round(performance.now() - started) }); } } fs.writeFileSync(output, JSON.stringify({ passed: results.length, results }, null, 2)); diff --git a/tests/windows-print-queue.test.js b/tests/windows-print-queue.test.js index 209490275..9c3730b6b 100644 --- a/tests/windows-print-queue.test.js +++ b/tests/windows-print-queue.test.js @@ -37,10 +37,10 @@ function rig(t) { tick: async (ms = 15000, q = queue) => { now += ms; await q.tick(); }, hold: () => { drain = false; } }; } -test('online: initialization has no paper feed and receipt bytes/cut are preserved', async t => { +test('online: receipt initializes in the same job without a separate wake delay', async t => { const r = rig(t); const result = await r.send(); assert.equal(result.status, 'Sent to printer'); assert.equal(result.success, true); - assert.equal(r.initialized.length, 1); + assert.equal(r.initialized.length, 0); assert.deepEqual([...r.calls[0].bytes], [27, 64, 65, 10, 29, 86, 0]); assert.equal(result.spoolerJobId, 1); assert.ok(r.events.some(e => e.status === 'Queued')); @@ -157,12 +157,16 @@ test('unknown USB presence and permission denied fail visibly without elevation' assert.equal(r.calls.length, 0); }); -test('retained initialization job blocks receipt and initialization does not repeat', async t => { - const r = rig(t); - r.transport.initialize = async job => { r.initialized.push(job); r.health.jobs.push({ id: 5, document: job.document, error: false, status: 'Spooling' }); return { success: true }; }; - assert.equal((await r.send()).status, 'Waiting'); await r.tick(); - assert.equal(r.initialized.length, 1); assert.equal(r.calls.length, 0); - r.health.jobs = []; await r.tick(); assert.equal(r.calls.length, 1); +test('retained wake job does not delay a ready receipt or create another wake', async t => { + const r = rig(t); r.health.printerStatus = 7; + r.transport.initialize = async job => { + r.initialized.push(job); r.health.printerStatus = 3; + r.health.jobs.push({ id: 5, document: job.document, error: false, status: 'Spooling' }); + return { success: true }; + }; + assert.equal((await r.send()).status, 'Sent to printer'); + assert.equal(r.initialized.length, 1); assert.equal(r.calls.length, 1); + await r.tick(); assert.equal(r.calls.length, 1); }); test('offline status with a present device can be initialized before readiness is rechecked', async t => { @@ -400,7 +404,7 @@ test('missing legacy payload is not automatically recreated or replayed', async }); test('startup failure is diagnosed accurately and pre-submission retries are bounded', async t => { - const r=rig(t);let attempts=0; + const r=rig(t);r.health.printerStatus=7;let attempts=0; r.transport.initialize=async()=>{attempts++;return {success:false,unavailable:true,submission:'not-submitted',error:'Print helper startup failed: READY deadline exceeded'};}; await r.send();for(let i=0;i<6;i++)await r.tick(60000); assert.equal(attempts,4);assert.equal(r.calls.length,0); @@ -411,13 +415,14 @@ test('startup failure is diagnosed accurately and pre-submission retries are bou test('controlled legacy recovery preserves a successful Reception copy and Kitchen identity', async t => { const r=rig(t);await r.send('reception','Reception'); const submit=r.transport.submit; + r.health.printerStatus=7; r.transport.initialize=async()=>({success:false,unavailable:true,error:'Print helper startup failed'}); await r.send('kitchen','Kitchen_New'); const job=r.queue.jobs.get(idFor('kitchen')); job.state='failed';job.reason='Printer initialization failed; check Windows queue';delete job.nonSubmissionVerified; r.queue.write(job.id+'.json',job); r.queue.stop();const restored=r.make(); - r.transport.initialize=async()=>({success:true});r.transport.submit=submit; + r.health.printerStatus=3; r.transport.initialize=async()=>({success:true});r.transport.submit=submit; const before={...job.binding}; const result=await restored.recoverUnsubmitted(job.id); assert.equal(result.success,true);assert.equal(result.jobId,job.id); @@ -443,13 +448,13 @@ test('failure before receipt submission is retried once helper recovers; failure }); test('optional retained initialization cannot starve healthy receipts indefinitely',async t=>{ const r=rig(t);r.transport.initialize=async job=>{r.initialized.push(job);r.health.jobs.push({id:99,document:job.document,status:'Spooling',error:false});return {success:true};}; - await r.send();assert.equal(r.calls.length,0);await r.tick(30001); - assert.equal(r.calls.length,1);assert.equal(r.initialized.length,1); + await r.send();assert.equal(r.calls.length,1);assert.equal(r.initialized.length,0); + await r.tick(0);assert.equal(r.calls.length,1); }); test('unknown idle pulse suppresses further optional traffic but not healthy receipts',async t=>{ const r=rig(t);r.queue.keepAlive.kitchen_printer={at:0,pending:true,document:'Posnic-idle-old',status:'unknown'}; - await r.send();assert.equal(r.calls.length,0);await r.tick(30001); - assert.equal(r.calls.length,1);assert.equal(r.queue.keepAlive.kitchen_printer.suppressed,true); + await r.send();assert.equal(r.calls.length,1); + assert.equal(r.queue.keepAlive.kitchen_printer.suppressed,true); }); test('a failed Windows queue job still blocks receipts after optional wake deadline',async t=>{ const r=rig(t);r.queue.keepAlive.kitchen_printer={at:0,pending:true,document:'Posnic-idle-old',status:'unknown'};