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

157 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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