import { createProjectFaviconCache, createProjectFaviconImageLoader, PROJECT_FAVICON_MAX_DATA_URL_LENGTH, PROJECT_FAVICON_THUMBNAIL_SIZE, } from "@t3tools/client-runtime/project-favicon-cache"; const DATABASE_NAME = "t3code:project-favicons"; const DATABASE_VERSION = 2; const STORE_NAME = "images"; let database: Promise | undefined; function openDatabase() { return (database ??= new Promise((resolve, reject) => { const request = indexedDB.open(DATABASE_NAME, DATABASE_VERSION); request.addEventListener("upgradeneeded", () => { for (const name of request.result.objectStoreNames) { if (name !== STORE_NAME) request.result.deleteObjectStore(name); } if (!request.result.objectStoreNames.contains(STORE_NAME)) { request.result.createObjectStore(STORE_NAME); } }); request.addEventListener("success", () => resolve(request.result)); request.addEventListener("error", () => reject(request.error)); request.addEventListener("blocked", () => reject(new Error("Project icon cache is blocked."))); })); } function completed(transaction: IDBTransaction) { return new Promise((resolve, reject) => { transaction.addEventListener("complete", () => resolve()); transaction.addEventListener("abort", () => reject(transaction.error)); transaction.addEventListener("error", () => reject(transaction.error)); }); } async function withStore( mode: IDBTransactionMode, use: (store: IDBObjectStore) => IDBRequest | void, ) { const transaction = (await openDatabase()).transaction(STORE_NAME, mode); const request = use(transaction.objectStore(STORE_NAME)); await completed(transaction); return request?.result; } /** Rasterizes a bitmap that is too large to inline, retrying at half size. */ async function downscaleProjectFavicon( image: { readonly mimeType: string; readonly bytes: Uint8Array }, signal: AbortSignal, ) { const bitmap = await createImageBitmap(new Blob([image.bytes], { type: image.mimeType })); try { signal.throwIfAborted(); const canvas = document.createElement("canvas"); for (const size of [PROJECT_FAVICON_THUMBNAIL_SIZE, PROJECT_FAVICON_THUMBNAIL_SIZE / 2]) { const scale = Math.min(1, size / bitmap.width, size / bitmap.height); canvas.width = Math.max(1, Math.round(bitmap.width * scale)); canvas.height = Math.max(1, Math.round(bitmap.height * scale)); const context = canvas.getContext("2d"); if (!context) throw new Error("Canvas is unavailable."); context.clearRect(0, 0, canvas.width, canvas.height); context.drawImage(bitmap, 0, 0, canvas.width, canvas.height); const dataUrl = canvas.toDataURL("image/webp", 0.85); if (dataUrl.length <= PROJECT_FAVICON_MAX_DATA_URL_LENGTH) return dataUrl; } throw new Error("Project icon thumbnail exceeds the cache limit."); } finally { bitmap.close(); } } export const projectFaviconCache = createProjectFaviconCache({ storage: { list: async () => (await withStore("readonly", (store) => store.getAll())) ?? [], put: async (key, entry) => { await withStore("readwrite", (store) => store.put(entry, key)); }, remove: async (key) => { await withStore("readwrite", (store) => store.delete(key)); }, }, load: createProjectFaviconImageLoader({ downscale: downscaleProjectFavicon }), });