Skip to content

Instantly share code, notes, and snippets.

@KimmoHernborg
Created November 11, 2025 17:39
Show Gist options
  • Select an option

  • Save KimmoHernborg/3b819e755c99d4b3c11cb2609d9c5d4d to your computer and use it in GitHub Desktop.

Select an option

Save KimmoHernborg/3b819e755c99d4b3c11cb2609d9c5d4d to your computer and use it in GitHub Desktop.
LocalCache.ts
/*
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