Stirling-PDF/frontend/src/utils/ContentCache.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

173 lines
3.8 KiB
TypeScript
Raw Normal View History

2025-09-02 18:15:13 +01:00
/**
* Generic content cache with TTL and size limits
* Reusable for any cached data with configurable parameters
*/
const DEBUG = process.env.NODE_ENV === 'development';
interface CacheEntry<T> {
value: T;
timestamp: number;
}
export interface CacheConfig {
/** Time-to-live in milliseconds */
ttl: number;
/** Maximum number of cache entries */
maxSize: number;
/** Enable cleanup warnings in development */
enableWarnings?: boolean;
}
export class ContentCache<T> {
private cache = new Map<string, CacheEntry<T>>();
private hits = 0;
private misses = 0;
constructor(private readonly config: CacheConfig) {}
/**
* Get cached value if valid
*/
get(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) {
this.misses++;
return null;
}
// Check if expired
if (Date.now() - entry.timestamp > this.config.ttl) {
this.cache.delete(key);
this.misses++;
return null;
}
this.hits++;
return entry.value;
}
/**
* Set cached value
*/
set(key: string, value: T): void {
// Clean up before adding if at capacity
if (this.cache.size >= this.config.maxSize) {
this.evictOldest();
}
this.cache.set(key, {
value,
timestamp: Date.now()
});
}
/**
* Generate cache key from ArrayBuffer content
*/
generateKeyFromBuffer(data: ArrayBuffer): string {
// Use file size + hash of first/last bytes as cache key
const view = new Uint8Array(data);
const size = data.byteLength;
const start = Array.from(view.slice(0, 16)).join(',');
const end = Array.from(view.slice(-16)).join(',');
return `${size}-${this.simpleHash(start + end)}`;
}
/**
* Generate cache key from string content
*/
generateKeyFromString(content: string): string {
return this.simpleHash(content);
}
/**
* Check if key exists and is valid
*/
has(key: string): boolean {
return this.get(key) !== null;
}
/**
* Clear all cache entries
*/
clear(): void {
this.cache.clear();
this.hits = 0;
this.misses = 0;
}
/**
* Get cache statistics
*/
getStats(): {
size: number;
maxSize: number;
hitRate: number;
hits: number;
misses: number;
} {
const total = this.hits + this.misses;
const hitRate = total > 0 ? this.hits / total : 0;
return {
size: this.cache.size,
maxSize: this.config.maxSize,
hitRate,
hits: this.hits,
misses: this.misses
};
}
/**
* Cleanup expired entries
*/
cleanup(): void {
const now = Date.now();
let cleaned = 0;
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp > this.config.ttl) {
this.cache.delete(key);
cleaned++;
}
}
if (DEBUG && this.config.enableWarnings && this.cache.size > this.config.maxSize * 0.8) {
console.warn(`📦 ContentCache: High cache usage (${this.cache.size}/${this.config.maxSize}), cleaned ${cleaned} expired entries`);
}
}
/**
* Evict oldest entry when at capacity
*/
private evictOldest(): void {
let oldestKey: string | null = null;
let oldestTime = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (entry.timestamp < oldestTime) {
oldestTime = entry.timestamp;
oldestKey = key;
}
}
if (oldestKey) {
this.cache.delete(oldestKey);
}
}
/**
* Simple hash function for cache keys
*/
private simpleHash(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash).toString(36);
}
}