// PDF Export: lazy puppeteer loader + response cache + renderer // ── PDF Export: lazy puppeteer loader ──────────────────────────────────────── let _puppeteerBrowser = null; let _puppeteerLoading = false; async function getPuppeteerBrowser() { if (_puppeteerBrowser) return _puppeteerBrowser; if (_puppeteerLoading) { // Wait for in-flight launch await new Promise(r => setTimeout(r, 2000)); return _puppeteerBrowser; } _puppeteerLoading = true; try { const puppeteer = require("puppeteer"); _puppeteerBrowser = await puppeteer.launch({ headless: true, args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu"] }); console.log("[PDF] Puppeteer browser launched"); _puppeteerBrowser.on("disconnected", () => { console.log("[PDF] Browser disconnected — will relaunch on next request"); _puppeteerBrowser = null; _puppeteerLoading = false; }); } catch (e) { console.error("[PDF] Puppeteer unavailable:", e.message); _puppeteerBrowser = null; } _puppeteerLoading = false; return _puppeteerBrowser; } // PDF response cache: key = "asn:format" or "compare:asn1:asn2" → {buf, ts} const pdfCache = new Map(); const PDF_CACHE_TTL = 5 * 60 * 1000; // 5 minutes function pdfCacheGet(key) { const e = pdfCache.get(key); if (e && (Date.now() - e.ts) < PDF_CACHE_TTL) return e.buf; return null; } function pdfCacheSet(key, buf) { if (pdfCache.size > 100) pdfCache.delete(pdfCache.keys().next().value); pdfCache.set(key, { buf, ts: Date.now() }); } async function renderHtmlToPdf(html) { const browser = await getPuppeteerBrowser(); if (!browser) throw new Error('PDF renderer unavailable (puppeteer not installed)'); const page = await browser.newPage(); try { await page.setContent(html, { waitUntil: 'domcontentloaded', timeout: 15000 }); const buf = await page.pdf({ format: 'A4', printBackground: true, margin: { top: '18mm', bottom: '18mm', left: '16mm', right: '16mm' }, }); return buf; } finally { await page.close(); } } module.exports = { getPuppeteerBrowser, pdfCacheGet, pdfCacheSet, renderHtmlToPdf };