90 lines
2.9 KiB
TypeScript
90 lines
2.9 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* useAvatarCache — cache de avatares em IndexedDB.
|
|
*
|
|
* Chave de cache: `${jid}:${version}` (version = hash curto do avatarUrl no banco).
|
|
* Quando a versão muda (novo avatar no banco), a chave muda e o cache invalida
|
|
* automaticamente — sem TTL fixo, sem stale data visível ao usuário.
|
|
*
|
|
* Armazenamento: dataURL base64 do JPEG/WebP (compatível com <img src>).
|
|
* Limpeza: entradas com mais de 30 dias são removidas na abertura do DB.
|
|
*/
|
|
|
|
const DB_NAME = 'nw-avatars'
|
|
const STORE_NAME = 'blobs'
|
|
const DB_VERSION = 1
|
|
const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000 // 30 dias
|
|
|
|
interface CacheEntry {
|
|
key: string // `${jid}:${version}`
|
|
dataUrl: string
|
|
savedAt: number
|
|
}
|
|
|
|
let dbPromise: Promise<IDBDatabase> | null = null
|
|
|
|
function openDB(): Promise<IDBDatabase> {
|
|
if (dbPromise) return dbPromise
|
|
dbPromise = new Promise((resolve, reject) => {
|
|
const req = indexedDB.open(DB_NAME, DB_VERSION)
|
|
req.onupgradeneeded = () => {
|
|
const db = req.result
|
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
db.createObjectStore(STORE_NAME, { keyPath: 'key' })
|
|
}
|
|
}
|
|
req.onsuccess = () => {
|
|
const db = req.result
|
|
pruneOldEntries(db)
|
|
resolve(db)
|
|
}
|
|
req.onerror = () => { dbPromise = null; reject(req.error) }
|
|
})
|
|
return dbPromise
|
|
}
|
|
|
|
function pruneOldEntries(db: IDBDatabase): void {
|
|
const tx = db.transaction(STORE_NAME, 'readwrite')
|
|
const store = tx.objectStore(STORE_NAME)
|
|
const cutoff = Date.now() - MAX_AGE_MS
|
|
const req = store.openCursor()
|
|
req.onsuccess = () => {
|
|
const cursor = req.result
|
|
if (!cursor) return
|
|
if ((cursor.value as CacheEntry).savedAt < cutoff) cursor.delete()
|
|
cursor.continue()
|
|
}
|
|
}
|
|
|
|
export async function getAvatarFromCache(jid: string, version: string): Promise<string | null> {
|
|
if (typeof window === 'undefined') return null
|
|
try {
|
|
const db = await openDB()
|
|
const key = `${jid}:${version}`
|
|
return new Promise((resolve) => {
|
|
const tx = db.transaction(STORE_NAME, 'readonly')
|
|
const req = tx.objectStore(STORE_NAME).get(key)
|
|
req.onsuccess = () => resolve((req.result as CacheEntry | undefined)?.dataUrl ?? null)
|
|
req.onerror = () => resolve(null)
|
|
})
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export async function setAvatarInCache(jid: string, version: string, dataUrl: string): Promise<void> {
|
|
if (typeof window === 'undefined') return
|
|
try {
|
|
const db = await openDB()
|
|
const key = `${jid}:${version}`
|
|
await new Promise<void>((resolve, reject) => {
|
|
const tx = db.transaction(STORE_NAME, 'readwrite')
|
|
const entry: CacheEntry = { key, dataUrl, savedAt: Date.now() }
|
|
const req = tx.objectStore(STORE_NAME).put(entry)
|
|
req.onsuccess = () => resolve()
|
|
req.onerror = () => reject(req.error)
|
|
})
|
|
} catch { /* quota exceeded ou IDB indisponível — ignora silenciosamente */ }
|
|
}
|