Created
November 11, 2025 17:39
-
-
Save KimmoHernborg/3b819e755c99d4b3c11cb2609d9c5d4d to your computer and use it in GitHub Desktop.
LocalCache.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| Example usage: | |
| ```typescript | |
| import { cache } from './utils/cache'; | |
| // Simple get/set | |
| cache.set(['user', 123], userData); | |
| const user = cache.get(['user', 123]); | |
| // Fetch-or-cache pattern (like React Query) | |
| const data = await cache.getOrSet( | |
| ['api', 'users', userId], | |
| () => fetchUser(userId), | |
| 30 * 60 * 1000 // 30 minutes | |
| ); | |
| // Invalidate specific key | |
| cache.invalidate(['user', 123]); | |
| // Invalidate all user-related caches | |
| cache.invalidatePrefix(['user']); | |
| ``` | |
| */ | |
| interface CacheEntry<T> { | |
| value: T; | |
| timestamp: number; | |
| expiresAt: number; | |
| } | |
| type CacheKey = string | number; | |
| export const cacheTime1Hour = 3_600_000 as const; // 1 hour in milliseconds | |
| export const cacheTime24Hours = 86_400_000 as const; // 24 hours in milliseconds | |
| class LocalCache { | |
| private cache: Map<string, CacheEntry<any>> = new Map(); | |
| private defaultTTL: number; | |
| constructor(defaultTTL: number = cacheTime1Hour) { | |
| this.defaultTTL = defaultTTL; | |
| } | |
| /** | |
| * Generates a cache key from an array of values | |
| */ | |
| private generateKey(keyArray: CacheKey[]): string { | |
| return keyArray.map(String).join("|"); | |
| } | |
| /** | |
| * Sets a value in the cache with optional TTL | |
| * @param keyArray - Array of values to generate cache key | |
| * @param value - Value to cache | |
| * @param ttl - Time to live in milliseconds (defaults to 1 hour) | |
| */ | |
| set<T>(keyArray: CacheKey[], value: T, ttl?: number): void { | |
| const key = this.generateKey(keyArray); | |
| const now = Date.now(); | |
| const expirationTime = ttl ?? this.defaultTTL; | |
| this.cache.set(key, { | |
| value, | |
| timestamp: now, | |
| expiresAt: now + expirationTime, | |
| }); | |
| } | |
| /** | |
| * Gets a value from the cache if it exists and hasn't expired | |
| * @param keyArray - Array of values to generate cache key | |
| * @returns The cached value or undefined if not found or expired | |
| */ | |
| get<T>(keyArray: CacheKey[]): T | undefined { | |
| const key = this.generateKey(keyArray); | |
| const entry = this.cache.get(key); | |
| if (!this.isValid(keyArray)) { | |
| return undefined; | |
| } | |
| return entry.value as T; | |
| } | |
| /** | |
| * Gets a value or sets it if it doesn't exist | |
| * @param keyArray - Array of values to generate cache key | |
| * @param fetchFn - Function to fetch the value if not in cache | |
| * @param ttl - Time to live in milliseconds (defaults to 1 hour) | |
| * @returns The cached or freshly fetched value | |
| */ | |
| async getOrSet<T>( | |
| keyArray: CacheKey[], | |
| fetchFn: () => Promise<T> | T, | |
| ttl?: number | |
| ): Promise<T> { | |
| const cached = this.get<T>(keyArray); | |
| if (cached !== undefined) { | |
| return cached; | |
| } | |
| const value = await fetchFn(); | |
| this.set(keyArray, value, ttl); | |
| return value; | |
| } | |
| /** | |
| * Checks if a key exists and hasn't expired | |
| * @param keyArray - Array of values to generate cache key | |
| */ | |
| has(keyArray: CacheKey[]): boolean { | |
| const key = this.generateKey(keyArray); | |
| const entry = this.cache.get(key); | |
| if (!this.isValid(keyArray)) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| /** | |
| * Checks if a key exists and hasn't expired | |
| * @param keyArray - Array of values to generate cache key | |
| * @return boolean - is the cache entry valid | |
| */ | |
| isValid(keyArray: CacheKey[]): boolean { | |
| const key = this.generateKey(keyArray); | |
| const entry = this.cache.get(key); | |
| if (!entry) { | |
| return false; | |
| } | |
| const now = Date.now(); | |
| if (now > entry.expiresAt) { | |
| this.cache.delete(key); | |
| return false; | |
| } | |
| return true; | |
| } | |
| /** | |
| * Invalidates a specific cache entry | |
| * @param keyArray - Array of values to generate cache key | |
| */ | |
| invalidate(keyArray: CacheKey[]): boolean { | |
| const key = this.generateKey(keyArray); | |
| return this.cache.delete(key); | |
| } | |
| /** | |
| * Invalidates all cache entries matching a prefix | |
| * @param prefixArray - Array of values to match the beginning of cache keys | |
| */ | |
| invalidatePrefix(prefixArray: CacheKey[]): number { | |
| const prefix = JSON.stringify(prefixArray).slice(0, -1); // Remove closing bracket | |
| let count = 0; | |
| for (const key of this.cache.keys()) { | |
| if (key.startsWith(prefix)) { | |
| this.cache.delete(key); | |
| count++; | |
| } | |
| } | |
| return count; | |
| } | |
| /** | |
| * Invalidate all cache entries | |
| */ | |
| invalidateAll() { | |
| this.cache.clear(); | |
| } | |
| /** | |
| * Invalidate all expired entries | |
| */ | |
| purge() { | |
| const now = Date.now(); | |
| for (const [key, entry] of this.cache.entries()) { | |
| if (now > entry.expiresAt) { | |
| this.cache.delete(key); | |
| } | |
| } | |
| } | |
| /** | |
| * Gets cache statistics | |
| */ | |
| getStats() { | |
| const now = Date.now(); | |
| let expired = 0; | |
| let active = 0; | |
| for (const entry of this.cache.values()) { | |
| if (now > entry.expiresAt) { | |
| expired++; | |
| } else { | |
| active++; | |
| } | |
| } | |
| return { | |
| total: this.cache.size, | |
| active, | |
| expired, | |
| }; | |
| } | |
| } | |
| // Export a singleton instance | |
| export const cache = new LocalCache(); | |
| // Export the class for custom instances | |
| export { LocalCache }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment