chamesu
Version:
This package contains all packages of the chat application.
152 lines (114 loc) • 4.39 kB
text/typescript
/*
* 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<T> {
prefix: string,
suffix?: string,
seconds: number,
getKey?: (key: T) => string
}
export class RedisCacheEventEmitter<T extends string | number | Record<string, any>> extends EventEmitter {
protected expireHandler: Job | undefined;
protected expireMap: Record<string, any>;
constructor(protected redisDatabase: Redis, public readonly options: RedisExpireCacheOptions<T>) {
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<boolean> {
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<any> {
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<boolean> {
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;
}
}