/* * Copyright (c) 2022. * Author Peter Placzek (tada5hi) * For the full copyright and license information, * view the LICENSE file that was distributed with this source code. */ import {EventEmitter} from "events"; import {Job, scheduleJob} from "node-schedule"; import {Redis} from "ioredis"; export interface RedisExpireCacheOptions { prefix: string, suffix?: string, seconds: number, getKey?: (key: T) => string } export class RedisCacheEventEmitter> extends EventEmitter { protected expireHandler: Job | undefined; protected expireMap: Record; constructor(protected redisDatabase: Redis, public readonly options: RedisExpireCacheOptions) { super(); this.expireMap = {}; this.registerExpireHandler(); } protected registerExpireHandler() { if (this.expireHandlerIsRunning()) { return; } this.expireHandler = scheduleJob('* * * * * *', async () => { const timeNow = new Date().getTime(); const ttlPipeline = this.redisDatabase.pipeline(); for (let key in this.expireMap) { if (!this.expireMap.hasOwnProperty(key)) continue; ttlPipeline.ttl(key); } const result = await ttlPipeline.exec(); const dropPipeline = this.redisDatabase.pipeline(); let iteration: number = 0; for (let key in this.expireMap) { if (!this.expireMap.hasOwnProperty(key)) continue; if (this.expireMap[key] < timeNow) { const redisTime = result[iteration] ? result[iteration + 1] : -1; if (typeof redisTime !== 'number' || redisTime <= 0) { dropPipeline.del(key); delete this.expireMap[key]; this.emit('expired', key); } else { this.expireMap[key] = timeNow + redisTime; } } iteration = iteration + 2; } await dropPipeline.exec(); }); } public expireHandlerIsRunning(): boolean { return typeof this.expireHandler !== 'undefined' && this.expireHandler.nextInvocation() !== null; } public async isExpired(id: T): Promise { const key : string = this.getKey(id); if (key in this.expireMap) { return false; } const ttl = await this.redisDatabase.ttl(key); return ttl <= 0; } public async get(id: T): Promise { const key : string = this.getKey(id); try { let entry = await this.redisDatabase.get(key); if (entry === null) { return undefined; } return JSON.parse(entry); } catch (e) { return undefined; } } public async set(id: T, value?: any, seconds?: number) { const key : string = this.getKey(id); seconds = seconds ?? this.options.seconds; if(typeof value === 'undefined') { const expireSet: number = await this.redisDatabase.expire(key, seconds); if (expireSet === 0) { await this.redisDatabase.set(key, seconds * 1000, 'EX', seconds) } } else { await this.redisDatabase.set(key, JSON.stringify(value), 'EX', seconds) } this.expireMap[key] = new Date().getTime() + (seconds * 1000); } public async drop(id: T): Promise { const key : string = this.getKey(id); if (key in this.expireMap) { delete this.expireMap[key]; } return await this.redisDatabase.del(key) === 1; } public getKey(id?: T) { let prefix: string = this.options.prefix + '.cache'; if (typeof this.options.suffix !== 'undefined') { prefix += '.' + this.options.suffix; } if (typeof id === 'undefined') { return prefix; } if (typeof this.options.getKey !== 'undefined') { return prefix + ':' + this.options.getKey(id); } if (typeof id === 'string' || typeof id === 'number') { return prefix + ':' + id; } return prefix; } }