import { inject, injectable } from '../config/dependency-injector.config.js'; import * as config from '../environment.js'; import { Redis, RedisOptions, SentinelAddress } from 'ioredis'; import { REDIS_DB, REDIS_HOST, REDIS_PASSWORD, REDIS_PORT, REDIS_SENTINEL_MASTER_NAME, REDIS_SENTINEL_HOST_LIST, REDIS_SENTINEL_PASSWORD, REDIS_USERNAME } from '../environment.js'; import { internalError } from '../models/error.model.js'; import { LoggerService } from './logger.service.js'; import { EventEmitter } from 'events'; import Redlock from 'redlock'; @injectable() export class RedisService { protected readonly DEFAULT_TTL: number = 32 * 60 * 60 * 24; // 32 days protected redis: Redis; protected isConnected = false; public events: EventEmitter; constructor(@inject(LoggerService) protected logger: LoggerService) { this.events = new EventEmitter(); const redisOptions = this.loadRedisConfig(); this.redis = new Redis(redisOptions); this.redis.on('connect', () => { if (!this.isConnected) { this.logger.verbose('Connected to Redis'); } else { this.logger.verbose('Reconnected to Redis'); } this.isConnected = true; this.events.emit('redisConnected'); }); this.redis.on('error', (e) => this.logger.error('Error Redis', e)); this.redis.on('end', () => { this.isConnected = false; this.logger.warn('Redis disconnected'); }); } createRedlock(retryCount = -1, retryDelay = 200) { return new Redlock([this.redis], { driftFactor: 0.01, retryCount, retryDelay, retryJitter: 200 // Random variation in the time between retries. }); } public onReady(callback: () => void) { if (this.isConnected) { callback(); } this.events.on('redisConnected', callback); } /** * Retrieves all keys from Redis that match the specified pattern. * * @param pattern - The pattern to match against Redis keys. * @returns A promise that resolves to an array of matching keys. * @throws {internalRecordingError} If there is an error retrieving keys from Redis. */ async getKeys(pattern: string): Promise { let cursor = '0'; const keys: Set = new Set(); do { const [nextCursor, partialKeys] = await this.redis.scan(cursor, 'MATCH', pattern); partialKeys.forEach((key) => keys.add(key)); cursor = nextCursor; } while (cursor !== '0'); return Array.from(keys); } /** * Checks if a given key exists in the Redis store. * * @param {string} key - The key to check for existence. * @returns {Promise} - A promise that resolves to `true` if the key exists, otherwise `false`. */ async exists(key: string): Promise { const result = await this.get(key); return !!result; } get(key: string, hashKey?: string): Promise { try { if (hashKey) { return this.redis.hget(key, hashKey); } else { return this.redis.get(key); } } catch (error) { this.logger.error('Error getting value from Redis', error); throw internalError(error); } } // getAll(key: string): Promise> { // try { // return this.redis.hgetall(key); // } catch (error) { // this.logger.error('Error getting value from Redis', error); // throw internalError(error); // } // } // getDel(key: string): Promise { // try { // return this.redis.getdel(key); // } catch (error) { // this.logger.error('Error getting and deleting value from Redis', error); // throw internalError(error); // } // } /** * Sets a value in Redis with an optional TTL (time-to-live). * * @param {string} key - The key under which the value will be stored. * @param {any} value - The value to be stored. Can be a string, number, boolean, or object. * @param {boolean} [withTTL=true] - Whether to set a TTL for the key. Defaults to true. * @returns {Promise} - A promise that resolves to 'OK' if the operation is successful. * @throws {Error} - Throws an error if the value type is invalid or if there is an issue setting the value in Redis. */ async set(key: string, value: any, withTTL = true): Promise { try { const valueType = typeof value; if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') { if (withTTL) { await this.redis.set(key, value, 'EX', this.DEFAULT_TTL); } else { await this.redis.set(key, value); } } else if (valueType === 'object') { await this.redis.hmset(key, value); if (withTTL) await this.redis.expire(key, this.DEFAULT_TTL); } else { throw new Error('Invalid value type'); } return 'OK'; } catch (error) { this.logger.error('Error setting value in Redis', error); throw error; } } /** * Deletes a key from Redis. * @param key - The key to delete. * @param hashKey - The hash key to delete. If provided, it will delete the hash key from the hash stored at the given key. * @returns A promise that resolves to the number of keys deleted. */ delete(key: string, hashKey?: string): Promise { try { if (hashKey) { return this.redis.hdel(key, hashKey); } else { return this.redis.del(key); } } catch (error) { throw internalError(`Error deleting key from Redis ${error}`); } } quit() { this.redis.quit(); } async checkHealth() { return (await this.redis.ping()) === 'PONG'; } private loadRedisConfig(): RedisOptions { // Check if openviduCall module is enabled. If not, exit the process config.checkModuleEnabled(); //Check if Redis Sentinel is configured if (REDIS_SENTINEL_HOST_LIST) { const sentinels: Array = []; const sentinelHosts = REDIS_SENTINEL_HOST_LIST.split(','); sentinelHosts.forEach((host) => { const rawHost = host.split(':'); if (rawHost.length !== 2) { throw new Error('The Redis Sentinel host list is required'); } const hostName = rawHost[0]; const port = parseInt(rawHost[1]); sentinels.push({ host: hostName, port }); }); if (!REDIS_SENTINEL_PASSWORD) throw new Error('The Redis Sentinel password is required'); this.logger.verbose('Using Redis Sentinel'); return { sentinels, sentinelPassword: REDIS_SENTINEL_PASSWORD, username: REDIS_USERNAME, password: REDIS_PASSWORD, name: REDIS_SENTINEL_MASTER_NAME, db: Number(REDIS_DB) }; } else { this.logger.verbose('Using Redis standalone'); return { port: Number(REDIS_PORT), host: REDIS_HOST, username: REDIS_USERNAME, password: REDIS_PASSWORD, db: Number(REDIS_DB) }; } } }