When Valkey is unreachable, ioredis was holding cache.get() calls in the offline queue for the default 10s connectTimeout before failing. This caused admin/image-gen and text-gen pages to stall on every load. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
74 lines
1.8 KiB
TypeScript
74 lines
1.8 KiB
TypeScript
/**
|
|
* Generic Valkey (Redis-compatible) cache.
|
|
*
|
|
* Reuses the same ioredis singleton from presignCache.ts but exposes a
|
|
* simple typed get/set/invalidate API for arbitrary JSON values.
|
|
*
|
|
* Usage:
|
|
* const books = await cache.get<Book[]>('books:all');
|
|
* await cache.set('books:all', books, 5 * 60);
|
|
* await cache.invalidate('books:all');
|
|
*/
|
|
|
|
import Redis from 'ioredis';
|
|
|
|
let _client: Redis | null = null;
|
|
|
|
function client(): Redis {
|
|
if (!_client) {
|
|
const url = process.env.VALKEY_URL ?? 'redis://valkey:6379';
|
|
_client = new Redis(url, {
|
|
lazyConnect: false,
|
|
enableOfflineQueue: true,
|
|
maxRetriesPerRequest: 1,
|
|
connectTimeout: 1500
|
|
});
|
|
_client.on('error', (err: Error) => {
|
|
console.error('[cache] Valkey error:', err.message);
|
|
});
|
|
}
|
|
return _client;
|
|
}
|
|
|
|
/** Return the cached value for key, or null if absent / expired / error. */
|
|
export async function get<T>(key: string): Promise<T | null> {
|
|
try {
|
|
const raw = await client().get(key);
|
|
if (!raw) return null;
|
|
return JSON.parse(raw) as T;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store a value under key for ttlSeconds seconds.
|
|
* Silently no-ops on Valkey errors so callers never crash.
|
|
*/
|
|
export async function set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {
|
|
try {
|
|
await client().set(key, JSON.stringify(value), 'EX', ttlSeconds);
|
|
} catch {
|
|
// non-fatal
|
|
}
|
|
}
|
|
|
|
/** Delete a key immediately (e.g. after a write that invalidates it). */
|
|
export async function invalidate(key: string): Promise<void> {
|
|
try {
|
|
await client().del(key);
|
|
} catch {
|
|
// non-fatal
|
|
}
|
|
}
|
|
|
|
/** Invalidate all keys matching a glob pattern (e.g. 'books:*'). */
|
|
export async function invalidatePattern(pattern: string): Promise<void> {
|
|
try {
|
|
const keys = await client().keys(pattern);
|
|
if (keys.length > 0) await client().del(...keys);
|
|
} catch {
|
|
// non-fatal
|
|
}
|
|
}
|