Files
SHiNE-server/shine-UI/js/services/arweave-avatar-cache-service.js
T

157 lines
5.1 KiB
JavaScript

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('Ошибка очистки кэша аватарок'));
});
}