import pkg from "../../package.json"; import { parsePrefs, type RenderPrefsV1 } from "../pdf/model/prefs"; import { buildRenderModel, type RenderModel } from "../pdf/model/build"; import { vendorFromSnapshot } from "../pdf/model/vendor"; import { Superseded } from "../pdf/render/client"; import type { RenderResult } from "../pdf/render/jobs"; import { api } from "./api"; import { loadLogoImages, logoForModel, type LogoImages } from "./logo"; import { getRenderClient, nextRenderJobId } from "./renderClient"; import type { BankAccount, Invoice, Settings } from "./types"; /** * The render model for an invoice whose images are already loaded (the live preview caches them). * Supplier and bank come from the snapshots frozen at issue; live settings are only a fallback. */ export function modelFromAssets( invoice: Invoice, settings: Settings, bank: BankAccount | null, logoImages: LogoImages | null, signatureDataUri: string | null, ): RenderModel { return buildRenderModel({ invoice, vendor: settings, bank, logo: logoForModel(vendorFromSnapshot(invoice.vendorSnapshot, settings), logoImages), signatureDataUri, appVersion: pkg.version, }); } /** Loads the invoice's logo and signature, then builds its render model. */ export async function buildModelForInvoice(invoice: Invoice, settings: Settings, bank: BankAccount | null): Promise { const vendor = vendorFromSnapshot(invoice.vendorSnapshot, settings); const logoImages = await loadLogoImages(vendor, api.readAssetDataUri); const signatureDataUri = invoice.signaturePath ? await api.readAssetDataUri(invoice.signaturePath) : null; return modelFromAssets(invoice, settings, bank, logoImages, signatureDataUri); } /** Page setup for an invoice: what was frozen at issue, else the user's current default. */ export const prefsForInvoice = (invoice: Invoice, settings: Settings): RenderPrefsV1 => parsePrefs(invoice.renderPrefs || settings.renderPrefs); /** * Renders an issued invoice to PDF bytes through the shared render client (worker, or the main * thread if the worker cannot start), with the layout audit issues and fingerprint. */ export async function renderInvoicePdf( invoice: Invoice, settings: Settings, bank: BankAccount | null, prefs: RenderPrefsV1 = prefsForInvoice(invoice, settings), ): Promise { const model = await buildModelForInvoice(invoice, settings, bank); return renderModelWithRetry(model, prefs); } /** Renders a ready model in the shared client. */ export async function renderModelWithRetry(model: RenderModel, prefs: RenderPrefsV1): Promise { // The live preview shares the client; if it takes the worker first this render is superseded, so go again. for (let attempt = 0; ; attempt++) { try { return await getRenderClient().render({ id: nextRenderJobId(), model, prefs }); } catch (err) { if (!(err instanceof Superseded) || attempt >= 4) throw err; } } }