| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 | import config from "config";import redis from "redis";import mongoose from "mongoose";import CoreClass from "../../core";// Lightweight / convenience wrapper around redis module for our needsconst pubs = {};const subs = {};let CacheModule;class _CacheModule extends CoreClass {	// eslint-disable-next-line require-jsdoc	constructor() {		super("cache");		CacheModule = this;	}	/**	 * Initialises the cache/redis module	 *	 * @returns {Promise} - returns promise (reject, resolve)	 */	async initialize() {		const importSchema = schemaName =>			new Promise(resolve => {				import(`./schemas/${schemaName}`).then(schema => resolve(schema.default));			});		this.schemas = {			session: await importSchema("session"),			station: await importSchema("station"),			playlist: await importSchema("playlist"),			officialPlaylist: await importSchema("officialPlaylist"),			song: await importSchema("song"),			punishment: await importSchema("punishment"),			recentActivity: await importSchema("recentActivity")		};		return new Promise((resolve, reject) => {			this.url = config.get("redis").url;			this.password = config.get("redis").password;			this.log("INFO", "Connecting...");			this.client = redis.createClient({				url: this.url,				password: this.password,				retry_strategy: options => {					if (this.getStatus() === "LOCKDOWN") return;					if (this.getStatus() !== "RECONNECTING") this.setStatus("RECONNECTING");					this.log("INFO", `Attempting to reconnect.`);					if (options.attempt >= 10) {						this.log("ERROR", `Stopped trying to reconnect.`);						this.setStatus("FAILED");						// this.failed = true;						// this._lockdown();					}				}			});			this.client.on("error", err => {				if (this.getStatus() === "INITIALIZING") reject(err);				if (this.getStatus() === "LOCKDOWN") return;				this.log("ERROR", `Error ${err.message}.`);			});			this.client.on("connect", () => {				this.log("INFO", "Connected succesfully.");				if (this.getStatus() === "INITIALIZING") resolve();				else if (this.getStatus() === "FAILED" || this.getStatus() === "RECONNECTING") this.setStatus("READY");			});		});	}	/**	 * Quits redis client	 *	 * @returns {Promise} - returns promise (reject, resolve)	 */	QUIT() {		return new Promise(resolve => {			if (CacheModule.client.connected) {				CacheModule.client.quit();				Object.keys(pubs).forEach(channel => pubs[channel].quit());				Object.keys(subs).forEach(channel => subs[channel].client.quit());			}			resolve();		});	}	/**	 * Sets a single value in a table	 *	 * @param {object} payload - object containing payload	 * @param {string} payload.table - name of the table we want to set a key of (table === redis hash)	 * @param {string} payload.key -  name of the key to set	 * @param {*} payload.value - the value we want to set	 * @param {boolean} [payload.stringifyJson=true] - stringify 'value' if it's an Object or Array	 * @returns {Promise} - returns a promise (resolve, reject)	 */	HSET(payload) {		return new Promise((resolve, reject) => {			let { key } = payload;			let { value } = payload;			if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();			// automatically stringify objects and arrays into JSON			if (["object", "array"].includes(typeof value)) value = JSON.stringify(value);			CacheModule.client.hset(payload.table, key, value, err => {				if (err) return reject(new Error(err));				return resolve(JSON.parse(value));			});		});	}	/**	 * Gets a single value from a table	 *	 * @param {object} payload - object containing payload	 * @param {string} payload.table - name of the table to get the value from (table === redis hash)	 * @param {string} payload.key - name of the key to fetch	 * @param {boolean} [payload.parseJson=true] - attempt to parse returned data as JSON	 * @returns {Promise} - returns a promise (resolve, reject)	 */	HGET(payload) {		return new Promise((resolve, reject) => {			let { key } = payload;			if (!key) return reject(new Error("Invalid key!"));			if (!payload.table) return reject(new Error("Invalid table!"));			if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();			return CacheModule.client.hget(payload.table, key, (err, value) => {				if (err) return reject(new Error(err));				try {					value = JSON.parse(value);				} catch (e) {					return reject(err);				}				return resolve(value);			});		});	}	/**	 * Deletes a single value from a table	 *	 * @param {object} payload - object containing payload	 * @param {string} payload.table - name of the table to delete the value from (table === redis hash)	 * @param {string} payload.key - name of the key to delete	 * @returns {Promise} - returns a promise (resolve, reject)	 */	HDEL(payload) {		return new Promise((resolve, reject) => {			// if (!payload.key || !table || typeof key !== "string")			// return cb(null, null);			let { key } = payload;			if (!payload.table) return reject(new Error("Invalid table!"));			if (!key) return reject(new Error("Invalid key!"));			if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();			return CacheModule.client.hdel(payload.table, key, err => {				if (err) return reject(new Error(err));				return resolve();			});		});	}	/**	 * Returns all the keys for a table	 *	 * @param {object} payload - object containing payload	 * @param {string} payload.table - name of the table to get the values from (table === redis hash)	 * @param {boolean} [payload.parseJson=true] - attempts to parse all values as JSON by default	 * @returns {Promise} - returns a promise (resolve, reject)	 */	HGETALL(payload) {		return new Promise((resolve, reject) => {			if (!payload.table) return reject(new Error("Invalid table!"));			return CacheModule.client.hgetall(payload.table, (err, obj) => {				if (err) return reject(new Error(err));				if (obj)					Object.keys(obj).forEach(key => {						obj[key] = JSON.parse(obj[key]);					});				else if (!obj) obj = [];				return resolve(obj);			});		});	}	/**	 * Publish a message to a channel, caches the redis client connection	 *	 * @param {object} payload - object containing payload	 * @param {string} payload.channel - the name of the channel we want to publish a message to	 * @param {*} payload.value - the value we want to send	 * @param {boolean} [payload.stringifyJson=true] - stringify 'value' if it's an Object or Array	 * @returns {Promise} - returns a promise (resolve, reject)	 */	PUB(payload) {		return new Promise((resolve, reject) => {			/* if (pubs[channel] === undefined) {            pubs[channel] = redis.createClient({ url: CacheModule.url });            pubs[channel].on('error', (err) => console.error);            } */			let { value } = payload;			if (!payload.channel) return reject(new Error("Invalid channel!"));			if (!value) return reject(new Error("Invalid value!"));			if (["object", "array"].includes(typeof value)) value = JSON.stringify(value);			// pubs[channel].publish(channel, value);			return CacheModule.client.publish(payload.channel, value, err => {				if (err) reject(err);				else resolve();			});		});	}	/**	 * Subscribe to a channel, caches the redis client connection	 *	 * @param {object} payload - object containing payload	 * @param {string} payload.channel - name of the channel to subscribe to	 * @param {boolean} [payload.parseJson=true] - parse the message as JSON	 * @returns {Promise} - returns a promise (resolve, reject)	 */	SUB(payload) {		return new Promise((resolve, reject) => {			if (!payload.channel) return reject(new Error("Invalid channel!"));			if (subs[payload.channel] === undefined) {				subs[payload.channel] = {					client: redis.createClient({						url: CacheModule.url,						password: CacheModule.password					}),					cbs: []				};				subs[payload.channel].client.on("message", (channel, message) => {					if (message.startsWith("[") || message.startsWith("{"))						try {							message = JSON.parse(message);						} catch (err) {							console.error(err);						}					else if (message.startsWith('"') && message.endsWith('"'))						message = message.substring(1).substring(0, message.length - 2);					return subs[channel].cbs.forEach(cb => cb(message));				});				subs[payload.channel].client.subscribe(payload.channel);			}			subs[payload.channel].cbs.push(payload.cb);			return resolve();		});	}	/**	 * Returns a redis schema	 *	 * @param {object} payload - object containing the payload	 * @param {string} payload.schemaName - the name of the schema to get	 * @returns {Promise} - returns promise (reject, resolve)	 */	GET_SCHEMA(payload) {		return new Promise(resolve => {			resolve(CacheModule.schemas[payload.schemaName]);		});	}}export default new _CacheModule();
 |