import { TRPCError } from "@trpc/server"; import type Redis from "ioredis"; import puppeteer, { type PDFOptions } from "puppeteer-core"; import { env } from "~/env"; import type { AnnunciId } from "~/schemas/public/Annunci"; import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe"; import { getKeydbClient } from "~/utils/keydb"; import { TOKEN_CONFIG } from "../auth/configs"; export const SCHEDA_ANNUNCIO_CACHE_PREFIX = "scheda-annuncio-"; const CONDIZIONI_CACHE_PREFIX = "condizioni-"; export const invalidateCache = async (cacheKey: string) => { const keybd = getKeydbClient(); if (keybd) { await keybd.del(cacheKey); } }; async function deleteByPrefix(redis: Redis, prefix: string) { // Create a stream to find keys matching the pattern const stream = redis.scanStream({ match: `${prefix}*`, count: 100, // Process 100 keys per iteration }); stream.on("data", async (keys: string[]) => { if (keys.length > 0) { // Create a pipeline to delete keys in a single network round-trip const pipeline = redis.pipeline(); // Use UNLINK instead of DEL for better performance on large keys keys.forEach((key) => pipeline.unlink(key)); await pipeline.exec(); console.log(`Deleted ${keys.length} keys`); } }); stream.on("error", (err) => { console.error( `Error scanning/unlinking keys with prefix ${prefix}: ${err.message}`, ); }); } export const invalidateAllCaches = async () => { const keybd = getKeydbClient(); if (keybd) { await deleteByPrefix(keybd, SCHEDA_ANNUNCIO_CACHE_PREFIX); await deleteByPrefix(keybd, CONDIZIONI_CACHE_PREFIX); } }; export const getCacheStats = async () => { const keybd = getKeydbClient(); if (keybd) { const schedaKeys = await keybd.keys(`${SCHEDA_ANNUNCIO_CACHE_PREFIX}*`); const condizioniKeys = await keybd.keys(`${CONDIZIONI_CACHE_PREFIX}*`); return { success: true, schedaCount: schedaKeys.length, condizioniCount: condizioniKeys.length, }; } return { success: false, schedaCount: 0, condizioniCount: 0, }; }; export const genPdfSchedaAnnuncioBase64 = async (annuncioId: AnnunciId) => { const pdf = await genPdfSchedaAnnuncio(annuncioId); const pdfBase64 = Buffer.from(pdf).toString("base64"); const base64Content = pdfBase64.includes("base64,") ? pdfBase64.split("base64,")[1] : pdfBase64; if (!base64Content) { throw new Error("Failed to convert PDF to base64"); } return base64Content; }; export const genPdfSchedaAnnuncio = async (annuncioId: AnnunciId) => { const keybd = getKeydbClient(); const url = `/area-riservata/scheda-annuncio-stampa/${annuncioId}?raw=true`; const options: PDFOptions = { printBackground: true }; if (keybd) { const cacheKey = `${SCHEDA_ANNUNCIO_CACHE_PREFIX}${annuncioId}`; const cached = await keybd.get(cacheKey); if (cached) { return new Uint8Array(Buffer.from(cached, "base64")); } const scheda = await puppeteerGenPdf({ url, options, }); await keybd.set( cacheKey, Buffer.from(scheda).toString("base64"), "EX", 3600 * 5, ); return scheda; } return await puppeteerGenPdf({ url, options }); }; export const genPdfCondizioniBase64 = async ( condizioniId: TestiEStringheStingaId, ) => { const pdf = await genPdfCondizioni(condizioniId); const pdfBase64 = Buffer.from(pdf).toString("base64"); const base64Content = pdfBase64.includes("base64,") ? pdfBase64.split("base64,")[1] : pdfBase64; if (!base64Content) { throw new Error("Failed to convert PDF to base64"); } return base64Content; }; const genPdfCondizioni = async (condizioniId: TestiEStringheStingaId) => { const keybd = getKeydbClient(); const url = `/servizio/condizioni/${condizioniId}?raw=true`; if (keybd) { const cacheKey = `${CONDIZIONI_CACHE_PREFIX}${condizioniId}`; const cached = await keybd.get(cacheKey); if (cached) { return new Uint8Array(Buffer.from(cached, "base64")); } const scheda = await puppeteerGenPdf({ url }); await keybd.set( cacheKey, Buffer.from(scheda).toString("base64"), "EX", 3600 * 24 * 5, ); return scheda; } return await puppeteerGenPdf({ url }); }; export const genPdfRicevutaCortesia = async (ordineId: OrdiniOrdineId) => { const url = `/servizio/ricevuta-cortesia/${ordineId}?raw=true`; return await puppeteerGenPdf({ url }); }; export const genPdfRicevutaCortesiaBase64 = async ( ordineId: OrdiniOrdineId, ) => { const pdf = await genPdfRicevutaCortesia(ordineId); const pdfBase64 = Buffer.from(pdf).toString("base64"); const base64Content = pdfBase64.includes("base64,") ? pdfBase64.split("base64,")[1] : pdfBase64; if (!base64Content) { throw new Error("Failed to convert PDF to base64"); } return base64Content; }; const puppeteerGenPdf = async ({ url, options, }: { url: string; options?: PDFOptions; }) => { let browser = null; try { browser = await puppeteer.connect({ browserWSEndpoint: `${env.BROWSER_WS_URL}?--user-data-dir=/tmp/session-0`, }); const page = await browser.newPage(); const c = await browser.cookies(); if ( c.length === 0 || !c.some( (cookie) => cookie.name === TOKEN_CONFIG.OVERRIDE_TOKEN_COOKIE_NAME, ) ) { const productionDomain = new URL(env.INTERNAL_BASE_URL).hostname; const newCookies = [ { name: TOKEN_CONFIG.OVERRIDE_TOKEN_COOKIE_NAME, value: env.EXP_API_PASS, domain: env.NODE_ENV === "production" ? productionDomain : "host.docker.internal", path: "/", }, ]; await browser.setCookie(...newCookies); } const baseUrl = env.NODE_ENV === "production" ? env.INTERNAL_BASE_URL : `http://host.docker.internal:3000`; const targetUrl = `${baseUrl}${url}`; try { await page.goto(targetUrl, { waitUntil: "networkidle2", timeout: 60000 }); } catch (e) { if ((e as Error).message.includes("detached")) { await page.goto(targetUrl, { waitUntil: "load", timeout: 60000 }); } else throw e; } const pdf = await page.pdf({ format: "A4", ...options, }); return pdf; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Errore nella generazione del PDF, URL: ${url}, ${(error as Error).message}`, }); } finally { // 'disconnect' tells browserless you're done with this tab if (browser) await browser.disconnect(); } };