first version
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
import { loadConfig } from '../config';
|
||||
|
||||
export interface CacheEntry<T> {
|
||||
data: T;
|
||||
timestamp: number;
|
||||
size: number; // Size in bytes
|
||||
}
|
||||
|
||||
export interface CacheStats {
|
||||
totalSize: number; // Total size in bytes
|
||||
entryCount: number;
|
||||
oldestEntry: number | null;
|
||||
newestEntry: number | null;
|
||||
}
|
||||
|
||||
class VaultCache {
|
||||
private readonly CACHE_KEY = 'vaultApiCache';
|
||||
private cache: Map<string, CacheEntry<unknown>>;
|
||||
|
||||
constructor() {
|
||||
this.cache = this.loadFromStorage();
|
||||
}
|
||||
|
||||
private loadFromStorage(): Map<string, CacheEntry<unknown>> {
|
||||
try {
|
||||
const stored = localStorage.getItem(this.CACHE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
return new Map(Object.entries(parsed));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load cache from storage:', error);
|
||||
}
|
||||
return new Map();
|
||||
}
|
||||
|
||||
private saveToStorage(): void {
|
||||
try {
|
||||
const obj = Object.fromEntries(this.cache);
|
||||
localStorage.setItem(this.CACHE_KEY, JSON.stringify(obj));
|
||||
} catch (error) {
|
||||
console.error('Failed to save cache to storage:', error);
|
||||
// If quota exceeded, clear old entries and retry
|
||||
this.evictOldEntries(0.5); // Remove 50% of entries
|
||||
try {
|
||||
const obj = Object.fromEntries(this.cache);
|
||||
localStorage.setItem(this.CACHE_KEY, JSON.stringify(obj));
|
||||
} catch (retryError) {
|
||||
console.error('Failed to save cache after cleanup:', retryError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private calculateSize(data: unknown): number {
|
||||
// Rough estimation of size in bytes
|
||||
return new Blob([JSON.stringify(data)]).size;
|
||||
}
|
||||
|
||||
private evictOldEntries(fraction: number): void {
|
||||
const entries = Array.from(this.cache.entries());
|
||||
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
||||
const toRemove = Math.floor(entries.length * fraction);
|
||||
for (let i = 0; i < toRemove; i++) {
|
||||
this.cache.delete(entries[i][0]);
|
||||
}
|
||||
}
|
||||
|
||||
private enforceSizeLimit(): void {
|
||||
const config = loadConfig();
|
||||
if (!config.cache.enabled) return;
|
||||
|
||||
const maxBytes = config.cache.maxSizeMB * 1024 * 1024;
|
||||
let totalSize = 0;
|
||||
|
||||
// Calculate total size
|
||||
for (const entry of this.cache.values()) {
|
||||
totalSize += entry.size;
|
||||
}
|
||||
|
||||
// If over limit, remove oldest entries
|
||||
if (totalSize > maxBytes) {
|
||||
const entries = Array.from(this.cache.entries());
|
||||
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
||||
|
||||
for (const [key, entry] of entries) {
|
||||
if (totalSize <= maxBytes * 0.8) break; // Remove until 80% of limit
|
||||
totalSize -= entry.size;
|
||||
this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get<T>(key: string): T | null {
|
||||
const config = loadConfig();
|
||||
if (!config.cache.enabled) return null;
|
||||
|
||||
const entry = this.cache.get(key) as CacheEntry<T> | undefined;
|
||||
if (!entry) return null;
|
||||
|
||||
// Check if entry is expired
|
||||
const age = Date.now() - entry.timestamp;
|
||||
if (age > config.cache.maxAge) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.data;
|
||||
}
|
||||
|
||||
set<T>(key: string, data: T): void {
|
||||
const config = loadConfig();
|
||||
if (!config.cache.enabled) return;
|
||||
|
||||
const size = this.calculateSize(data);
|
||||
const entry: CacheEntry<T> = {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
size,
|
||||
};
|
||||
|
||||
this.cache.set(key, entry as CacheEntry<unknown>);
|
||||
this.enforceSizeLimit();
|
||||
this.saveToStorage();
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
const config = loadConfig();
|
||||
if (!config.cache.enabled) return false;
|
||||
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) return false;
|
||||
|
||||
const age = Date.now() - entry.timestamp;
|
||||
if (age > config.cache.maxAge) {
|
||||
this.cache.delete(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
delete(key: string): void {
|
||||
this.cache.delete(key);
|
||||
this.saveToStorage();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
this.saveToStorage();
|
||||
}
|
||||
|
||||
getStats(): CacheStats {
|
||||
let totalSize = 0;
|
||||
let oldestEntry: number | null = null;
|
||||
let newestEntry: number | null = null;
|
||||
|
||||
for (const entry of this.cache.values()) {
|
||||
totalSize += entry.size;
|
||||
if (oldestEntry === null || entry.timestamp < oldestEntry) {
|
||||
oldestEntry = entry.timestamp;
|
||||
}
|
||||
if (newestEntry === null || entry.timestamp > newestEntry) {
|
||||
newestEntry = entry.timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalSize,
|
||||
entryCount: this.cache.size,
|
||||
oldestEntry,
|
||||
newestEntry,
|
||||
};
|
||||
}
|
||||
|
||||
// Clean up expired entries
|
||||
cleanup(): void {
|
||||
const config = loadConfig();
|
||||
const now = Date.now();
|
||||
const keysToDelete: string[] = [];
|
||||
|
||||
for (const [key, entry] of this.cache.entries()) {
|
||||
if (now - entry.timestamp > config.cache.maxAge) {
|
||||
keysToDelete.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of keysToDelete) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
|
||||
if (keysToDelete.length > 0) {
|
||||
this.saveToStorage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const vaultCache = new VaultCache();
|
||||
|
||||
// Cleanup expired entries on page load
|
||||
vaultCache.cleanup();
|
||||
|
||||
Reference in New Issue
Block a user