import db from "../../db.server"; const MAX_BYTES = 5 * 1024 * 1024; // 5 MB cap const STALE_AFTER_MS = 24 * 60 * 60 * 1000; // re-fetch once a day at most /** * Returns a `data:` URL for the shop's logo bytes, fetching from the * configured URL on first use (or when stale) and persisting to the * `LogoCache` table for subsequent renders. */ export async function getLogoDataUrl( shopDomain: string, logoUrl: string, ): Promise { if (!logoUrl) return undefined; const cached = await db.logoCache.findUnique({ where: { shopDomain } }); const isFresh = cached && cached.sourceUrl === logoUrl && Date.now() - cached.fetchedAt.getTime() < STALE_AFTER_MS; if (isFresh && cached) { return toDataUrl(cached.bytes, cached.contentType); } let response: Response; try { response = await fetch(logoUrl); } catch (err) { console.warn(`Logo fetch failed for ${shopDomain}:`, err); return cached ? toDataUrl(cached.bytes, cached.contentType) : undefined; } if (!response.ok) { console.warn(`Logo fetch HTTP ${response.status} for ${shopDomain}`); return cached ? toDataUrl(cached.bytes, cached.contentType) : undefined; } const arrayBuf = await response.arrayBuffer(); if (arrayBuf.byteLength > MAX_BYTES) { console.warn(`Logo too large (${arrayBuf.byteLength} bytes) — skipping cache.`); return undefined; } const bytes = Buffer.from(arrayBuf); const contentType = response.headers.get("content-type") || guessContentType(logoUrl); const etag = response.headers.get("etag") || ""; await db.logoCache.upsert({ where: { shopDomain }, create: { shopDomain, sourceUrl: logoUrl, bytes, contentType, etag }, update: { sourceUrl: logoUrl, bytes, contentType, etag, fetchedAt: new Date() }, }); return toDataUrl(bytes, contentType); } function toDataUrl(bytes: Buffer | Uint8Array, contentType: string): string { const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes); return `data:${contentType};base64,${buf.toString("base64")}`; } function guessContentType(url: string): string { const lower = url.toLowerCase(); if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg"; if (lower.endsWith(".webp")) return "image/webp"; return "image/png"; }