infoalloggi-monorepo/apps/infoalloggi/src/server/services/puppeteer.service.ts

216 lines
5.8 KiB
TypeScript
Raw Normal View History

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,
});
const page = await browser.newPage();
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}`;
await page.goto(targetUrl, { waitUntil: "networkidle0" });
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: ${(error as Error).message}`,
});
} finally {
// 'disconnect' tells browserless you're done with this tab
if (browser) await browser.disconnect();
}
};