first version

This commit is contained in:
2025-10-20 18:45:52 +02:00
commit 19eebd72df
41 changed files with 8080 additions and 0 deletions
+202
View File
@@ -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();