import ApplicationComponent from "../ApplicationComponent"; import {Express} from "express"; import redis, {RedisClient} from "redis"; import config from "config"; import {logger} from "../Logger"; import session, {Store} from "express-session"; import CacheProvider from "../CacheProvider"; export default class RedisComponent extends ApplicationComponent implements CacheProvider { private redisClient?: RedisClient; private store?: Store; public async start(_app: Express): Promise { this.redisClient = redis.createClient(config.get('redis.port'), config.get('redis.host'), { password: config.has('redis.password') ? config.get('redis.password') : undefined, }); this.redisClient.on('error', (err: Error) => { logger.error(err, 'An error occurred with redis.'); }); this.store = new RedisStore(this); } public async stop(): Promise { const redisClient = this.redisClient; if (redisClient) { await this.close('Redis connection', callback => redisClient.quit(callback)); } } public getStore(): Store { if (!this.store) throw `Redis store was not initialized.`; return this.store; } public canServe(): boolean { return this.redisClient !== undefined && this.redisClient.connected; } public async get(key: string, defaultValue?: T): Promise { return await new Promise((resolve, reject) => { if (!this.redisClient) { reject(`Redis client was not initialized.`); return; } this.redisClient.get(key, (err, val) => { if (err) { reject(err); return; } resolve((val || defaultValue || undefined) as T); }); }); } public async has(key: string): Promise { return await this.get(key) !== undefined; } public async forget(key: string): Promise { return await new Promise((resolve, reject) => { if (!this.redisClient) { reject(`Redis client was not initialized.`); return; } this.redisClient.del(key, (err) => { if (err) { reject(err); return; } resolve(); }); }); } public async remember(key: string, value: string, ttl: number): Promise { return await new Promise((resolve, reject) => { if (!this.redisClient) { reject(`Redis client was not initialized.`); return; } this.redisClient.psetex(key, ttl, value, (err) => { if (err) return reject(err); resolve(); }); }); } public async persist(key: string, ttl: number): Promise { return await new Promise((resolve, reject) => { if (!this.redisClient) { reject(`Redis client was not initialized.`); return; } this.redisClient.pexpire(key, ttl, (err) => { if (err) return reject(err); resolve(); }); }); } } class RedisStore extends Store { private readonly redisComponent: RedisComponent; public constructor(redisComponent: RedisComponent) { super(); this.redisComponent = redisComponent; } public get(sid: string, callback: (err?: Error, session?: (session.SessionData | null)) => void): void { this.redisComponent.get(`-session:${sid}`) .then(value => { if (value) { this.redisComponent.persist(`-session:${sid}`, config.get('session.cookie.maxAge')) .then(() => { callback(undefined, JSON.parse(value)); }) .catch(callback); } else { callback(undefined, null); } }) .catch(callback); } public set(sid: string, session: session.SessionData, callback?: (err?: Error) => void): void { this.redisComponent.remember(`-session:${sid}`, JSON.stringify(session), config.get('session.cookie.maxAge')) .then(() => { if (callback) callback(); }) .catch(callback); } public destroy(sid: string, callback?: (err?: Error) => void): void { this.redisComponent.forget(`-session:${sid}`) .then(() => { if (callback) callback(); }) .catch(callback); } }