import { buildArweaveDataUrl, validateArweaveTxId } from './arweave-file-service.js'; const DB_NAME = 'shine-ui-avatar-cache'; const DB_VERSION = 1; const STORE = 'avatars'; const MAX_ITEMS = 200; const MAX_BYTES = 50 * 1024 * 1024; function openDb() { return new Promise((resolve, reject) => { if (typeof indexedDB === 'undefined') { reject(new Error('IndexedDB недоступен')); return; } const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = () => { const db = request.result; if (!db.objectStoreNames.contains(STORE)) { db.createObjectStore(STORE, { keyPath: 'txId' }); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error || new Error('Ошибка открытия IndexedDB')); }); } async function withStore(mode, runner) { const db = await openDb(); try { return await new Promise((resolve, reject) => { const tx = db.transaction(STORE, mode); const store = tx.objectStore(STORE); const result = runner(store, tx, resolve, reject); if (result !== undefined) resolve(result); tx.onerror = () => reject(tx.error || new Error('Ошибка транзакции IndexedDB')); }); } finally { db.close(); } } async function getRecord(txId) { return withStore('readonly', (store, _tx, resolve, reject) => { const req = store.get(txId); req.onsuccess = () => resolve(req.result || null); req.onerror = () => reject(req.error || new Error('Ошибка чтения кэша аватарки')); }); } async function putRecord(record) { return withStore('readwrite', (store, tx, resolve, reject) => { store.put(record); tx.oncomplete = () => resolve(true); tx.onerror = () => reject(tx.error || new Error('Ошибка записи кэша аватарки')); }); } async function getAllRecords() { return withStore('readonly', (store, _tx, resolve, reject) => { const req = store.getAll(); req.onsuccess = () => resolve(Array.isArray(req.result) ? req.result : []); req.onerror = () => reject(req.error || new Error('Ошибка чтения списка кэша аватарок')); }); } async function deleteRecords(keys) { if (!Array.isArray(keys) || !keys.length) return; await withStore('readwrite', (store, tx, resolve, reject) => { keys.forEach((key) => store.delete(key)); tx.oncomplete = () => resolve(true); tx.onerror = () => reject(tx.error || new Error('Ошибка очистки кэша аватарок')); }); } async function ensureCacheLimits() { const records = await getAllRecords(); if (!records.length) return; let totalBytes = 0; records.forEach((row) => { totalBytes += Number(row?.sizeBytes || row?.blob?.size || 0); }); if (records.length <= MAX_ITEMS && totalBytes <= MAX_BYTES) return; const ordered = [...records].sort((a, b) => Number(a?.cachedAtMs || 0) - Number(b?.cachedAtMs || 0)); const deleteKeys = []; let keepCount = records.length; let keepBytes = totalBytes; for (let i = 0; i < ordered.length; i += 1) { if (keepCount <= MAX_ITEMS && keepBytes <= MAX_BYTES) break; const row = ordered[i]; deleteKeys.push(String(row?.txId || '')); keepCount -= 1; keepBytes -= Number(row?.sizeBytes || row?.blob?.size || 0); } await deleteRecords(deleteKeys.filter(Boolean)); } async function fetchAvatarBlob({ gateway, txId }) { const url = buildArweaveDataUrl({ gateway, txId }); const response = await fetch(url, { method: 'GET' }); if (!response.ok) { throw new Error(`Не удалось загрузить аватар (${response.status} ${response.statusText})`); } const blob = await response.blob(); const type = String(blob?.type || '').toLowerCase(); if (!type.startsWith('image/')) { throw new Error('Arweave-файл не является изображением'); } return blob; } async function getBlobFromCacheOrGateway({ gateway, txId }) { try { const cached = await getRecord(txId); if (cached?.blob instanceof Blob) { return cached.blob; } } catch { // ignore IndexedDB errors and fallback to fetch } const blob = await fetchAvatarBlob({ gateway, txId }); const record = { txId, blob, contentType: String(blob.type || 'application/octet-stream'), sizeBytes: Number(blob.size || 0), cachedAtMs: Date.now(), }; try { await putRecord(record); await ensureCacheLimits(); } catch { // ignore cache write errors } return blob; } export async function getCachedAvatarObjectUrl({ gateway, txId }) { const cleanTxId = String(txId || '').trim(); if (!validateArweaveTxId(cleanTxId)) { throw new Error('Некорректный Transaction ID Arweave'); } const blob = await getBlobFromCacheOrGateway({ gateway, txId: cleanTxId }); return URL.createObjectURL(blob); } export async function clearAvatarCache() { await withStore('readwrite', (store, tx, resolve, reject) => { store.clear(); tx.oncomplete = () => resolve(true); tx.onerror = () => reject(tx.error || new Error('Ошибка очистки кэша аватарок')); }); }