interface CachedItem { data: T; timestamp: number; } export const decodeStorageKey = (storageKey: string) => atob(storageKey.split("_")[1]); export class PersistentCache { private readonly keyPrefix: string; private readonly expiryHours: number; private readonly keysSetKey: `local:${string}`; constructor(keyPrefix: string, expiryHours: number) { this.keyPrefix = keyPrefix; this.expiryHours = expiryHours; this.keysSetKey = `local:${keyPrefix}_keys`; } private getCacheKey(key: string): `local:${string}` { const safeKey = btoa(key); return `local:${this.keyPrefix}_${safeKey}`; } private async getStoredKeys(): Promise> { const keys = await storage.getItem(this.keysSetKey); return new Set(keys || []); } private async addKeyToSet(key: string): Promise { const keys = await this.getStoredKeys(); keys.add(key); await storage.setItem(this.keysSetKey, Array.from(keys)); } private async removeKeyFromSet(...key: string[]): Promise { const keys = await this.getStoredKeys(); for (const k of key) keys.delete(k); await storage.setItem(this.keysSetKey, Array.from(keys)); } async get(key: string): Promise { const cacheKey = this.getCacheKey(key); const cached = await storage.getItem>(cacheKey); if (!cached) return undefined; const now = Date.now(); const expiryTime = cached.timestamp + this.expiryHours * 60 * 60 * 1000; if (this.expiryHours > 0 && now > expiryTime) { await storage.removeItem(cacheKey); return undefined; } return cached.data; } async set(key: string, value: T): Promise { const cacheKey = this.getCacheKey(key); const cachedItem: CachedItem = { data: value, timestamp: Date.now(), }; await storage.setItem(cacheKey, cachedItem); await this.addKeyToSet(key); } async remove(key: string): Promise { const cacheKey = this.getCacheKey(key); await storage.removeItem(cacheKey); await this.removeKeyFromSet(key); } async getAll(): Promise> { const keys = await this.getStoredKeys(); if (keys.size === 0) { return new Map(); } const cacheKeys = Array.from(keys).map((key) => this.getCacheKey(key)); const items = await storage.getItems(cacheKeys); const result = new Map(); const now = Date.now(); const keysToRemove: string[] = []; for (const { key, value } of items) { const expiryTime = value.timestamp + this.expiryHours * 60 * 60 * 1000; if (this.expiryHours > 0 && now > expiryTime) { keysToRemove.push(key); } else { result.set(key, value.data); } } // Clean up expired or missing items if (keysToRemove.length > 0) { const expiredCacheKeys = keysToRemove.map((key) => this.getCacheKey(key)); await Promise.all([ storage.removeItems(expiredCacheKeys), this.removeKeyFromSet(...keysToRemove), ]); } return result; } }