v1.0.0-alpha4: Add Redis/Valkey caching and search with principal agent auth for global settings

This commit is contained in:
2026-01-01 23:58:00 +00:00
parent f98ff8fcec
commit 71e5070e6c
10 changed files with 1012 additions and 6 deletions
+156
View File
@@ -0,0 +1,156 @@
import Redis from 'ioredis';
// Redis/Valkey connection instance
let redisClient: Redis | null = null;
/**
* Get or create Redis connection
* Uses REDIS_URL from environment or defaults to localhost
*/
export function getRedisClient(): Redis {
if (!redisClient) {
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
redisClient = new Redis(redisUrl, {
maxRetriesPerRequest: 3,
retryStrategy(times) {
const delay = Math.min(times * 50, 2000);
return delay;
},
reconnectOnError(err) {
const targetErrors = ['READONLY', 'ECONNRESET', 'ETIMEDOUT'];
return targetErrors.some(targetError => err.message.includes(targetError));
}
});
redisClient.on('error', (err) => {
console.error('Redis connection error:', { message: err.message });
});
redisClient.on('connect', () => {
console.log('Redis connected successfully');
});
}
return redisClient;
}
/**
* Cache helper functions
*/
interface CacheOptions {
ttl?: number; // Time to live in seconds (default: 300 = 5 minutes)
}
/**
* Get cached data by key
*/
export async function getCached<T>(key: string): Promise<T | null> {
try {
const redis = getRedisClient();
const data = await redis.get(key);
if (!data) {
return null;
}
return JSON.parse(data) as T;
} catch (err) {
console.error('Cache get error:', { key, message: (err as Error).message });
return null;
}
}
/**
* Set cached data with optional TTL
*/
export async function setCached(key: string, data: any, options: CacheOptions = {}): Promise<boolean> {
try {
const redis = getRedisClient();
const ttl = options.ttl || 300; // Default 5 minutes
const serialized = JSON.stringify(data);
await redis.setex(key, ttl, serialized);
return true;
} catch (err) {
console.error('Cache set error:', { key, message: (err as Error).message });
return false;
}
}
/**
* Delete cached data by key or pattern
*/
export async function deleteCached(keyOrPattern: string): Promise<number> {
try {
const redis = getRedisClient();
// If it contains wildcards, use scan + delete
if (keyOrPattern.includes('*')) {
let cursor = '0';
let deletedCount = 0;
do {
const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', keyOrPattern, 'COUNT', 100);
cursor = nextCursor;
if (keys.length > 0) {
const deleted = await redis.del(...keys);
deletedCount += deleted;
}
} while (cursor !== '0');
return deletedCount;
} else {
// Direct key deletion
return await redis.del(keyOrPattern);
}
} catch (err) {
console.error('Cache delete error:', { keyOrPattern, message: (err as Error).message });
return 0;
}
}
/**
* Search functionality using Redis
*/
export interface SearchOptions {
fields?: string[]; // Fields to search in
limit?: number; // Max results
fuzzy?: boolean; // Enable fuzzy matching
}
/**
* Cache search results
*/
export async function cacheSearchResults(query: string, results: any[], options: CacheOptions = {}): Promise<boolean> {
const cacheKey = `search:${query.toLowerCase().trim()}`;
return setCached(cacheKey, results, options);
}
/**
* Get cached search results
*/
export async function getCachedSearchResults(query: string): Promise<any[] | null> {
const cacheKey = `search:${query.toLowerCase().trim()}`;
return getCached<any[]>(cacheKey);
}
/**
* Invalidate all search caches
*/
export async function invalidateSearchCache(): Promise<number> {
return deleteCached('search:*');
}
/**
* Close Redis connection (for cleanup)
*/
export async function closeRedisConnection(): Promise<void> {
if (redisClient) {
await redisClient.quit();
redisClient = null;
}
}