add redis conn to config

This commit is contained in:
miloschwartz 2025-06-13 16:42:15 -04:00
parent 21f4623e3e
commit 139c9d2ce3
No known key found for this signature in database
4 changed files with 420 additions and 288 deletions

88
package-lock.json generated
View file

@ -58,6 +58,7 @@
"http-errors": "2.0.0", "http-errors": "2.0.0",
"i": "^0.3.7", "i": "^0.3.7",
"input-otp": "1.4.2", "input-otp": "1.4.2",
"ioredis": "^5.6.1",
"jmespath": "^0.16.0", "jmespath": "^0.16.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
@ -1971,6 +1972,12 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
}, },
"node_modules/@ioredis/commands": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==",
"license": "MIT"
},
"node_modules/@isaacs/balanced-match": { "node_modules/@isaacs/balanced-match": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
@ -6254,6 +6261,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/cmdk": { "node_modules/cmdk": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
@ -6691,6 +6707,15 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -8853,6 +8878,30 @@
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
}, },
"node_modules/ioredis": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz",
"integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -9810,12 +9859,24 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"license": "MIT"
},
"node_modules/lodash.includes": { "node_modules/lodash.includes": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
"license": "MIT"
},
"node_modules/lodash.isboolean": { "node_modules/lodash.isboolean": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
@ -14402,6 +14463,27 @@
"node": ">=0.8.8" "node": ">=0.8.8"
} }
}, },
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"license": "MIT",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@ -15173,6 +15255,12 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
"license": "MIT"
},
"node_modules/statuses": { "node_modules/statuses": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",

View file

@ -75,6 +75,7 @@
"http-errors": "2.0.0", "http-errors": "2.0.0",
"i": "^0.3.7", "i": "^0.3.7",
"input-otp": "1.4.2", "input-otp": "1.4.2",
"ioredis": "^5.6.1",
"jmespath": "^0.16.0", "jmespath": "^0.16.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",

View file

@ -1,14 +1,6 @@
import Redis from 'ioredis'; import Redis, { RedisOptions } from "ioredis";
import logger from '@server/logger'; import logger from "@server/logger";
import config from "@server/lib/config";
interface RedisConfig {
host: string;
port: number;
password?: string;
db?: number;
retryDelayOnFailover?: number;
maxRetriesPerRequest?: number;
}
class RedisManager { class RedisManager {
private static instance: RedisManager; private static instance: RedisManager;
@ -16,10 +8,13 @@ class RedisManager {
private subscriber: Redis | null = null; private subscriber: Redis | null = null;
private publisher: Redis | null = null; private publisher: Redis | null = null;
private isEnabled: boolean = false; private isEnabled: boolean = false;
private subscribers: Map<string, Set<(channel: string, message: string) => void>> = new Map(); private subscribers: Map<
string,
Set<(channel: string, message: string) => void>
> = new Map();
private constructor() { private constructor() {
this.isEnabled = !!process.env.REDIS; this.isEnabled = config.getRawConfig().redis?.enabled || false;
if (this.isEnabled) { if (this.isEnabled) {
this.initializeClients(); this.initializeClients();
} }
@ -32,15 +27,18 @@ class RedisManager {
return RedisManager.instance; return RedisManager.instance;
} }
private getRedisConfig(): RedisConfig { private getRedisConfig(): RedisOptions {
return { const redisConfig = config.getRawConfig().redis!;
host: process.env.REDIS_HOST || 'localhost', const opts: RedisOptions = {
port: parseInt(process.env.REDIS_PORT || '6379'), host: redisConfig.host!,
password: process.env.REDIS_PASSWORD, port: redisConfig.port!,
db: parseInt(process.env.REDIS_DB || '0'), password: redisConfig.password,
retryDelayOnFailover: 100, db: redisConfig.db,
maxRetriesPerRequest: 3, tls: {
rejectUnauthorized: false
},
}; };
return opts;
} }
private initializeClients(): void { private initializeClients(): void {
@ -57,48 +55,54 @@ class RedisManager {
this.subscriber = new Redis(config); this.subscriber = new Redis(config);
// Set up error handlers // Set up error handlers
this.client.on('error', (err) => { this.client.on("error", (err) => {
logger.error('Redis client error:', err); logger.error("Redis client error:", err);
}); });
this.publisher.on('error', (err) => { this.publisher.on("error", (err) => {
logger.error('Redis publisher error:', err); logger.error("Redis publisher error:", err);
}); });
this.subscriber.on('error', (err) => { this.subscriber.on("error", (err) => {
logger.error('Redis subscriber error:', err); logger.error("Redis subscriber error:", err);
}); });
// Set up connection handlers // Set up connection handlers
this.client.on('connect', () => { this.client.on("connect", () => {
logger.info('Redis client connected'); logger.info("Redis client connected");
}); });
this.publisher.on('connect', () => { this.publisher.on("connect", () => {
logger.info('Redis publisher connected'); logger.info("Redis publisher connected");
}); });
this.subscriber.on('connect', () => { this.subscriber.on("connect", () => {
logger.info('Redis subscriber connected'); logger.info("Redis subscriber connected");
}); });
// Set up message handler for subscriber // Set up message handler for subscriber
this.subscriber.on('message', (channel: string, message: string) => { this.subscriber.on(
"message",
(channel: string, message: string) => {
const channelSubscribers = this.subscribers.get(channel); const channelSubscribers = this.subscribers.get(channel);
if (channelSubscribers) { if (channelSubscribers) {
channelSubscribers.forEach(callback => { channelSubscribers.forEach((callback) => {
try { try {
callback(channel, message); callback(channel, message);
} catch (error) { } catch (error) {
logger.error(`Error in subscriber callback for channel ${channel}:`, error); logger.error(
`Error in subscriber callback for channel ${channel}:`,
error
);
} }
}); });
} }
}); }
);
logger.info('Redis clients initialized successfully'); logger.info("Redis clients initialized successfully");
} catch (error) { } catch (error) {
logger.error('Failed to initialize Redis clients:', error); logger.error("Failed to initialize Redis clients:", error);
this.isEnabled = false; this.isEnabled = false;
} }
} }
@ -111,7 +115,11 @@ class RedisManager {
return this.client; return this.client;
} }
public async set(key: string, value: string, ttl?: number): Promise<boolean> { public async set(
key: string,
value: string,
ttl?: number
): Promise<boolean> {
if (!this.isRedisEnabled() || !this.client) return false; if (!this.isRedisEnabled() || !this.client) return false;
try { try {
@ -122,7 +130,7 @@ class RedisManager {
} }
return true; return true;
} catch (error) { } catch (error) {
logger.error('Redis SET error:', error); logger.error("Redis SET error:", error);
return false; return false;
} }
} }
@ -133,7 +141,7 @@ class RedisManager {
try { try {
return await this.client.get(key); return await this.client.get(key);
} catch (error) { } catch (error) {
logger.error('Redis GET error:', error); logger.error("Redis GET error:", error);
return null; return null;
} }
} }
@ -145,7 +153,7 @@ class RedisManager {
await this.client.del(key); await this.client.del(key);
return true; return true;
} catch (error) { } catch (error) {
logger.error('Redis DEL error:', error); logger.error("Redis DEL error:", error);
return false; return false;
} }
} }
@ -157,7 +165,7 @@ class RedisManager {
await this.client.sadd(key, member); await this.client.sadd(key, member);
return true; return true;
} catch (error) { } catch (error) {
logger.error('Redis SADD error:', error); logger.error("Redis SADD error:", error);
return false; return false;
} }
} }
@ -169,7 +177,7 @@ class RedisManager {
await this.client.srem(key, member); await this.client.srem(key, member);
return true; return true;
} catch (error) { } catch (error) {
logger.error('Redis SREM error:', error); logger.error("Redis SREM error:", error);
return false; return false;
} }
} }
@ -180,19 +188,23 @@ class RedisManager {
try { try {
return await this.client.smembers(key); return await this.client.smembers(key);
} catch (error) { } catch (error) {
logger.error('Redis SMEMBERS error:', error); logger.error("Redis SMEMBERS error:", error);
return []; return [];
} }
} }
public async hset(key: string, field: string, value: string): Promise<boolean> { public async hset(
key: string,
field: string,
value: string
): Promise<boolean> {
if (!this.isRedisEnabled() || !this.client) return false; if (!this.isRedisEnabled() || !this.client) return false;
try { try {
await this.client.hset(key, field, value); await this.client.hset(key, field, value);
return true; return true;
} catch (error) { } catch (error) {
logger.error('Redis HSET error:', error); logger.error("Redis HSET error:", error);
return false; return false;
} }
} }
@ -203,7 +215,7 @@ class RedisManager {
try { try {
return await this.client.hget(key, field); return await this.client.hget(key, field);
} catch (error) { } catch (error) {
logger.error('Redis HGET error:', error); logger.error("Redis HGET error:", error);
return null; return null;
} }
} }
@ -215,7 +227,7 @@ class RedisManager {
await this.client.hdel(key, field); await this.client.hdel(key, field);
return true; return true;
} catch (error) { } catch (error) {
logger.error('Redis HDEL error:', error); logger.error("Redis HDEL error:", error);
return false; return false;
} }
} }
@ -226,7 +238,7 @@ class RedisManager {
try { try {
return await this.client.hgetall(key); return await this.client.hgetall(key);
} catch (error) { } catch (error) {
logger.error('Redis HGETALL error:', error); logger.error("Redis HGETALL error:", error);
return {}; return {};
} }
} }
@ -238,12 +250,15 @@ class RedisManager {
await this.publisher.publish(channel, message); await this.publisher.publish(channel, message);
return true; return true;
} catch (error) { } catch (error) {
logger.error('Redis PUBLISH error:', error); logger.error("Redis PUBLISH error:", error);
return false; return false;
} }
} }
public async subscribe(channel: string, callback: (channel: string, message: string) => void): Promise<boolean> { public async subscribe(
channel: string,
callback: (channel: string, message: string) => void
): Promise<boolean> {
if (!this.isRedisEnabled() || !this.subscriber) return false; if (!this.isRedisEnabled() || !this.subscriber) return false;
try { try {
@ -257,12 +272,15 @@ class RedisManager {
this.subscribers.get(channel)!.add(callback); this.subscribers.get(channel)!.add(callback);
return true; return true;
} catch (error) { } catch (error) {
logger.error('Redis SUBSCRIBE error:', error); logger.error("Redis SUBSCRIBE error:", error);
return false; return false;
} }
} }
public async unsubscribe(channel: string, callback?: (channel: string, message: string) => void): Promise<boolean> { public async unsubscribe(
channel: string,
callback?: (channel: string, message: string) => void
): Promise<boolean> {
if (!this.isRedisEnabled() || !this.subscriber) return false; if (!this.isRedisEnabled() || !this.subscriber) return false;
try { try {
@ -284,7 +302,7 @@ class RedisManager {
return true; return true;
} catch (error) { } catch (error) {
logger.error('Redis UNSUBSCRIBE error:', error); logger.error("Redis UNSUBSCRIBE error:", error);
return false; return false;
} }
} }
@ -304,13 +322,12 @@ class RedisManager {
this.subscriber = null; this.subscriber = null;
} }
this.subscribers.clear(); this.subscribers.clear();
logger.info('Redis clients disconnected'); logger.info("Redis clients disconnected");
} catch (error) { } catch (error) {
logger.error('Error disconnecting Redis clients:', error); logger.error("Error disconnecting Redis clients:", error);
} }
} }
} }
// Export singleton instance
export const redisManager = RedisManager.getInstance(); export const redisManager = RedisManager.getInstance();
export default redisManager; export default redisManager;

View file

@ -131,6 +131,32 @@ export const configSchema = z.object({
.optional() .optional()
}) })
.optional(), .optional(),
redis: z
.object({
enabled: z.boolean(),
host: z.string().optional(),
port: portSchema.optional(),
password: z.string().optional(),
db: z.number().int().nonnegative().optional().default(0),
tls: z
.object({
rejectUnauthorized: z.boolean().optional().default(true)
})
.optional()
})
.refine(
(redis) => {
if (!redis.enabled) {
return true;
}
return redis.host !== undefined && redis.port !== undefined;
},
{
message:
"If Redis is enabled, connection details must be provided"
}
)
.optional(),
traefik: z traefik: z
.object({ .object({
http_entrypoint: z.string().optional().default("web"), http_entrypoint: z.string().optional().default("web"),