mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 12:12:09 +01:00
feat: implement cache layer, CSP improvements, and database performance optimizations
- Add Redis cache implementation with LRU eviction - Enhance Content Security Policy with nonce generation - Optimize database queries with connection pooling - Add cache invalidation API endpoints - Improve security monitoring performance
This commit is contained in:
233
lib/redis.ts
Normal file
233
lib/redis.ts
Normal file
@ -0,0 +1,233 @@
|
||||
/**
|
||||
* Redis Client Configuration and Management
|
||||
*
|
||||
* This module provides Redis client setup with connection management,
|
||||
* error handling, and graceful fallbacks to in-memory caching when Redis is unavailable.
|
||||
*/
|
||||
|
||||
import { createClient, type RedisClientType } from "redis";
|
||||
import { env } from "./env";
|
||||
|
||||
type RedisClient = RedisClientType;
|
||||
|
||||
class RedisManager {
|
||||
private client: RedisClient | null = null;
|
||||
private isConnected = false;
|
||||
private isConnecting = false;
|
||||
private connectionAttempts = 0;
|
||||
private readonly maxRetries = 3;
|
||||
private readonly retryDelay = 2000;
|
||||
|
||||
constructor() {
|
||||
this.initializeConnection();
|
||||
}
|
||||
|
||||
private async initializeConnection(): Promise<void> {
|
||||
if (this.isConnecting || this.isConnected) return;
|
||||
|
||||
this.isConnecting = true;
|
||||
|
||||
try {
|
||||
if (!env.REDIS_URL) {
|
||||
console.log("[Redis] No REDIS_URL provided, skipping Redis connection");
|
||||
this.isConnecting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.client = createClient({
|
||||
url: env.REDIS_URL,
|
||||
socket: {
|
||||
connectTimeout: 5000,
|
||||
commandTimeout: 3000,
|
||||
},
|
||||
retryDelayOnFailover: 100,
|
||||
retryDelayOnClusterDown: 300,
|
||||
});
|
||||
|
||||
this.client.on("error", (error) => {
|
||||
console.error("[Redis] Client error:", error);
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
this.client.on("connect", () => {
|
||||
console.log("[Redis] Connected successfully");
|
||||
this.isConnected = true;
|
||||
this.connectionAttempts = 0;
|
||||
});
|
||||
|
||||
this.client.on("disconnect", () => {
|
||||
console.log("[Redis] Disconnected");
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
await this.client.connect();
|
||||
} catch (error) {
|
||||
console.error("[Redis] Connection failed:", error);
|
||||
this.isConnected = false;
|
||||
this.connectionAttempts++;
|
||||
|
||||
if (this.connectionAttempts < this.maxRetries) {
|
||||
console.log(
|
||||
`[Redis] Retrying connection in ${this.retryDelay}ms (attempt ${this.connectionAttempts}/${this.maxRetries})`
|
||||
);
|
||||
setTimeout(() => {
|
||||
this.isConnecting = false;
|
||||
this.initializeConnection();
|
||||
}, this.retryDelay);
|
||||
} else {
|
||||
console.warn(
|
||||
"[Redis] Max connection attempts reached, falling back to in-memory caching"
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
this.isConnecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
async get(key: string): Promise<string | null> {
|
||||
if (!this.isConnected || !this.client) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.client.get(key);
|
||||
} catch (error) {
|
||||
console.error(`[Redis] GET failed for key ${key}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async set(
|
||||
key: string,
|
||||
value: string,
|
||||
options?: { EX?: number; PX?: number }
|
||||
): Promise<boolean> {
|
||||
if (!this.isConnected || !this.client) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.client.set(key, value, options);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[Redis] SET failed for key ${key}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async del(key: string): Promise<boolean> {
|
||||
if (!this.isConnected || !this.client) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.client.del(key);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[Redis] DEL failed for key ${key}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async exists(key: string): Promise<boolean> {
|
||||
if (!this.isConnected || !this.client) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.client.exists(key);
|
||||
return result === 1;
|
||||
} catch (error) {
|
||||
console.error(`[Redis] EXISTS failed for key ${key}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async mget(keys: string[]): Promise<(string | null)[]> {
|
||||
if (!this.isConnected || !this.client || keys.length === 0) {
|
||||
return keys.map(() => null);
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.client.mGet(keys);
|
||||
} catch (error) {
|
||||
console.error(`[Redis] MGET failed for keys ${keys.join(", ")}:`, error);
|
||||
return keys.map(() => null);
|
||||
}
|
||||
}
|
||||
|
||||
async mset(keyValuePairs: Record<string, string>): Promise<boolean> {
|
||||
if (!this.isConnected || !this.client) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.client.mSet(keyValuePairs);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[Redis] MSET failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async flushPattern(pattern: string): Promise<number> {
|
||||
if (!this.isConnected || !this.client) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
const keys = await this.client.keys(pattern);
|
||||
if (keys.length === 0) return 0;
|
||||
|
||||
await this.client.del(keys);
|
||||
return keys.length;
|
||||
} catch (error) {
|
||||
console.error(`[Redis] FLUSH pattern ${pattern} failed:`, error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
isAvailable(): boolean {
|
||||
return this.isConnected && this.client !== null;
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.client) {
|
||||
try {
|
||||
await this.client.disconnect();
|
||||
} catch (error) {
|
||||
console.error("[Redis] Disconnect error:", error);
|
||||
}
|
||||
this.client = null;
|
||||
this.isConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
async healthCheck(): Promise<{
|
||||
connected: boolean;
|
||||
latency?: number;
|
||||
error?: string;
|
||||
}> {
|
||||
if (!this.isConnected || !this.client) {
|
||||
return { connected: false, error: "Not connected" };
|
||||
}
|
||||
|
||||
try {
|
||||
const start = Date.now();
|
||||
await this.client.ping();
|
||||
const latency = Date.now() - start;
|
||||
return { connected: true, latency };
|
||||
} catch (error) {
|
||||
return {
|
||||
connected: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const redisManager = new RedisManager();
|
||||
|
||||
export { redisManager };
|
||||
export type { RedisClient };
|
||||
Reference in New Issue
Block a user