import * as ioredis from "ioredis"; import { CacheScriptEvaluator, CacheService, DeleteByPatternOptions, HashCacheService, ListCacheService, LuaCall, SetCacheOption, SetCacheService, } from "../interfaces"; import { SET_CACHE_POLICY } from "../constants"; export class RedisService implements CacheService, HashCacheService, ListCacheService, CacheScriptEvaluator, SetCacheService { protected _redis: ioredis.Redis; constructor(config: ioredis.RedisOptions) { this._redis = new ioredis.Redis(config); } public get(key: string): Promise { return this._redis.get(key); } public async getNumber(key: string): Promise { const result = await this._redis.get(key); if (result === null) { return null; } return this.convertToNumber(result); } public async deleteByPattern( deletePattern: string, options?: DeleteByPatternOptions ): Promise { let cursor = "0"; const prefix = this._redis.options.keyPrefix; let scanPattern = deletePattern; if (options?.includePrefixInScanPattern ?? true) { scanPattern = prefix + deletePattern; } do { // TODO: ScanStream for multiple-node Redis cluster const [nextCursor, keys] = await this._redis.scan( cursor, "MATCH", scanPattern, "COUNT", 100 // adjust batch size as needed ); cursor = nextCursor; if (keys.length > 0) { const dupPrefixRemovedKeys = prefix ? keys.map((k) => (k.startsWith(prefix) ? k.slice(prefix.length) : k)) : keys; // TODO: Chunk this for better load if (typeof (this._redis as any).unlink === "function") { await (this._redis as any).unlink(...dupPrefixRemovedKeys); } else { await this._redis.del(...dupPrefixRemovedKeys); } } } while (cursor !== "0"); } public async del(...keys: string[]): Promise { await this._redis.del(...keys); } public async multiEval(calls: LuaCall[]): Promise { const multi = this._redis.multi(); for (const [script, numberOfKeys, ...params] of calls) { if (numberOfKeys !== params.slice(0, numberOfKeys).length) { throw new Error( `numberOfKeys (${numberOfKeys}) does not match key count in params` ); } multi.eval(script, numberOfKeys, ...params); } const execResult = await multi.exec(); // bubble up the first Redis-side error, if any for (const [err] of execResult) { if (err) throw err; } // unwrap the values return execResult.map(([, value]) => value); } public async sadd(key: string, ...values: string[]): Promise { return this._redis.sadd(key, ...values); } public async srem(key: string, ...values: string[]): Promise { return this._redis.srem(key, ...values); } public async scard(key: string): Promise { return this._redis.scard(key); } public async eval(script: string, numberOfKeys: number, ...args: any[]) { return this._redis.eval(script, numberOfKeys, ...args); } public async hset(key: string, field: string, value: any): Promise { await this._redis.hset(key, { [field]: value, }); } public async hexists(key: string, field: string): Promise { return this._redis.hexists(key, field); } public async lpush(key: string, value: any): Promise { await this._redis.lpush(key, value); } public async lrem( key: string, count: string | number, element: string | Buffer | number ): Promise { return this._redis.lrem(key, count, element); } public async rpush(key: string, value: any): Promise { await this._redis.rpush(key, value); } public async lset(key: string, index: number, value: any): Promise { await this._redis.lset(key, index, value); } public async lrange( key: string, start: number, end: number ): Promise { return this._redis.lrange(key, start, end); } public async lindex(key: string, index: number): Promise { return this._redis.lindex(key, index); } public async llen(key: string): Promise { return this._redis.llen(key); } public async hget(key: string, field: string): Promise { return this._redis.hget(key, field); } public async hdel(key: string, ...fields: string[]): Promise { return this._redis.hdel(key, ...fields); } public async hincrby(key: string, field: string, value = 1): Promise { return this._redis.hincrby(key, field, value); } public async hincrbyfloat( key: string, field: string, value = 1 ): Promise { return this._redis.hincrbyfloat(key, field, value); } public async hkeys(key: string): Promise { return this._redis.hkeys(key); } public async ttl(key: string): Promise { return this._redis.ttl(key); } public async hlen(key: string): Promise { return this._redis.hlen(key); } public set(key: string, value: any, option?: SetCacheOption): Promise { if (!option) { return this._redis.set(key, value); } switch (option.policy) { case SET_CACHE_POLICY.WITH_TTL: return this._redis.set(key, value, "EX", option.value); // TTL in seconds case SET_CACHE_POLICY.KEEP_TTL: return this._redis.set(key, value, "KEEPTTL"); case SET_CACHE_POLICY.IF_EXISTS: // Set only if exists, with optional TTL return option.value ? this._redis.set(key, value, "EX", option.value, "XX") : this._redis.set(key, value, "XX"); case SET_CACHE_POLICY.IF_NOT_EXISTS: return option.value ? this._redis.set(key, value, "EX", option.value, "NX") : this._redis.set(key, value, "NX"); default: throw new Error("policy not supported"); } } public async incrBy(key: string, value = 1): Promise { const result = await this._redis.incrby(key, value); return this.convertToNumber(result); } protected convertToNumber(value: any): number { const number = parseFloat(value); if (isNaN(number)) { throw new Error(`Value "${value}" is not a valid number.`); } return number; } public expire(key: string, ttl: number): Promise { return this._redis.expire(key, ttl); } public async incrByFloat(key: string, value: number): Promise { const result = await this._redis.incrbyfloat(key, value); return this.convertToNumber(result); } public async decrBy(key: string, value = 1): Promise { const result = await this._redis.decrby(key, value); return this.convertToNumber(result); } }