index.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import config from "config";
  2. import redis from "redis";
  3. import mongoose from "mongoose";
  4. import CoreClass from "../../core";
  5. // Lightweight / convenience wrapper around redis module for our needs
  6. const pubs = {};
  7. const subs = {};
  8. class CacheModule extends CoreClass {
  9. // eslint-disable-next-line require-jsdoc
  10. constructor() {
  11. super("cache");
  12. }
  13. /**
  14. * Initialises the cache/redis module
  15. *
  16. * @returns {Promise} - returns promise (reject, resolve)
  17. */
  18. async initialize() {
  19. const importSchema = schemaName =>
  20. new Promise(resolve => {
  21. import(`./schemas/${schemaName}`).then(schema => resolve(schema.default));
  22. });
  23. this.schemas = {
  24. session: await importSchema("session"),
  25. station: await importSchema("station"),
  26. playlist: await importSchema("playlist"),
  27. officialPlaylist: await importSchema("officialPlaylist"),
  28. song: await importSchema("song"),
  29. punishment: await importSchema("punishment")
  30. };
  31. return new Promise((resolve, reject) => {
  32. this.url = config.get("redis").url;
  33. this.password = config.get("redis").password;
  34. this.log("INFO", "Connecting...");
  35. this.client = redis.createClient({
  36. url: this.url,
  37. password: this.password,
  38. retry_strategy: options => {
  39. if (this.getStatus() === "LOCKDOWN") return;
  40. if (this.getStatus() !== "RECONNECTING") this.setStatus("RECONNECTING");
  41. this.log("INFO", `Attempting to reconnect.`);
  42. if (options.attempt >= 10) {
  43. this.log("ERROR", `Stopped trying to reconnect.`);
  44. this.setStatus("FAILED");
  45. // this.failed = true;
  46. // this._lockdown();
  47. }
  48. }
  49. });
  50. this.client.on("error", err => {
  51. if (this.getStatus() === "INITIALIZING") reject(err);
  52. if (this.getStatus() === "LOCKDOWN") return;
  53. this.log("ERROR", `Error ${err.message}.`);
  54. });
  55. this.client.on("connect", () => {
  56. this.log("INFO", "Connected succesfully.");
  57. if (this.getStatus() === "INITIALIZING") resolve();
  58. else if (this.getStatus() === "FAILED" || this.getStatus() === "RECONNECTING") this.setStatus("READY");
  59. });
  60. });
  61. }
  62. /**
  63. * Quits redis client
  64. *
  65. * @returns {Promise} - returns promise (reject, resolve)
  66. */
  67. QUIT() {
  68. return new Promise(resolve => {
  69. if (this.client.connected) {
  70. this.client.quit();
  71. Object.keys(pubs).forEach(channel => pubs[channel].quit());
  72. Object.keys(subs).forEach(channel => subs[channel].client.quit());
  73. }
  74. resolve();
  75. });
  76. }
  77. /**
  78. * Sets a single value in a table
  79. *
  80. * @param {object} payload - object containing payload
  81. * @param {string} payload.table - name of the table we want to set a key of (table === redis hash)
  82. * @param {string} payload.key - name of the key to set
  83. * @param {*} payload.value - the value we want to set
  84. * @param {boolean} [payload.stringifyJson=true] - stringify 'value' if it's an Object or Array
  85. * @returns {Promise} - returns a promise (resolve, reject)
  86. */
  87. HSET(payload) {
  88. // table, key, value, cb, stringifyJson = true
  89. return new Promise((resolve, reject) => {
  90. let { key } = payload;
  91. let { value } = payload;
  92. if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
  93. // automatically stringify objects and arrays into JSON
  94. if (["object", "array"].includes(typeof value)) value = JSON.stringify(value);
  95. this.client.hset(payload.table, key, value, err => {
  96. if (err) return reject(new Error(err));
  97. return resolve(JSON.parse(value));
  98. });
  99. });
  100. }
  101. /**
  102. * Gets a single value from a table
  103. *
  104. * @param {object} payload - object containing payload
  105. * @param {string} payload.table - name of the table to get the value from (table === redis hash)
  106. * @param {string} payload.key - name of the key to fetch
  107. * @param {boolean} [payload.parseJson=true] - attempt to parse returned data as JSON
  108. * @returns {Promise} - returns a promise (resolve, reject)
  109. */
  110. HGET(payload) {
  111. // table, key, parseJson = true
  112. return new Promise((resolve, reject) => {
  113. let { key } = payload;
  114. if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
  115. this.client.hget(payload.table, key, (err, value) => {
  116. if (err) return reject(new Error(err));
  117. try {
  118. value = JSON.parse(value);
  119. } catch (e) {
  120. return reject(err);
  121. }
  122. return resolve(value);
  123. });
  124. });
  125. }
  126. /**
  127. * Deletes a single value from a table
  128. *
  129. * @param {object} payload - object containing payload
  130. * @param {string} payload.table - name of the table to delete the value from (table === redis hash)
  131. * @param {string} payload.key - name of the key to delete
  132. * @returns {Promise} - returns a promise (resolve, reject)
  133. */
  134. HDEL(payload) {
  135. // table, key, cb
  136. return new Promise((resolve, reject) => {
  137. // if (!payload.key || !table || typeof key !== "string")
  138. // return cb(null, null);
  139. let { key } = payload;
  140. if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
  141. this.client.hdel(payload.table, key, err => {
  142. if (err) return reject(new Error(err));
  143. return resolve();
  144. });
  145. });
  146. }
  147. /**
  148. * Returns all the keys for a table
  149. *
  150. * @param {object} payload - object containing payload
  151. * @param {string} payload.table - name of the table to get the values from (table === redis hash)
  152. * @param {boolean} [payload.parseJson=true] - attempts to parse all values as JSON by default
  153. * @returns {Promise} - returns a promise (resolve, reject)
  154. */
  155. HGETALL(payload) {
  156. // table, cb, parseJson = true
  157. return new Promise((resolve, reject) => {
  158. this.client.hgetall(payload.table, (err, obj) => {
  159. if (err) return reject(new Error(err));
  160. if (obj)
  161. Object.keys(obj).forEach(key => {
  162. obj[key] = JSON.parse(obj[key]);
  163. });
  164. else if (!obj) obj = [];
  165. return resolve(obj);
  166. });
  167. });
  168. }
  169. /**
  170. * Publish a message to a channel, caches the redis client connection
  171. *
  172. * @param {object} payload - object containing payload
  173. * @param {string} payload.channel - the name of the channel we want to publish a message to
  174. * @param {*} payload.value - the value we want to send
  175. * @param {boolean} [payload.stringifyJson=true] - stringify 'value' if it's an Object or Array
  176. * @returns {Promise} - returns a promise (resolve, reject)
  177. */
  178. PUB(payload) {
  179. // channel, value, stringifyJson = true
  180. return new Promise((resolve, reject) => {
  181. /* if (pubs[channel] === undefined) {
  182. pubs[channel] = redis.createClient({ url: this.url });
  183. pubs[channel].on('error', (err) => console.error);
  184. } */
  185. let { value } = payload;
  186. if (["object", "array"].includes(typeof value)) value = JSON.stringify(value);
  187. // pubs[channel].publish(channel, value);
  188. this.client.publish(payload.channel, value, err => {
  189. if (err) reject(err);
  190. else resolve();
  191. });
  192. });
  193. }
  194. /**
  195. * Subscribe to a channel, caches the redis client connection
  196. *
  197. * @param {object} payload - object containing payload
  198. * @param {string} payload.channel - name of the channel to subscribe to
  199. * @param {boolean} [payload.parseJson=true] - parse the message as JSON
  200. * @returns {Promise} - returns a promise (resolve, reject)
  201. */
  202. SUB(payload) {
  203. // channel, cb, parseJson = true
  204. return new Promise(resolve => {
  205. if (subs[payload.channel] === undefined) {
  206. subs[payload.channel] = {
  207. client: redis.createClient({
  208. url: this.url,
  209. password: this.password
  210. }),
  211. cbs: []
  212. };
  213. subs[payload.channel].client.on("message", (channel, message) => {
  214. try {
  215. message = JSON.parse(message);
  216. } catch (err) {
  217. console.error(err);
  218. }
  219. return subs[channel].cbs.forEach(cb => cb(message));
  220. });
  221. subs[payload.channel].client.subscribe(payload.channel);
  222. }
  223. subs[payload.channel].cbs.push(payload.cb);
  224. resolve();
  225. });
  226. }
  227. /**
  228. * Returns a redis schema
  229. *
  230. * @param {object} payload - object containing the payload
  231. * @param {string} payload.schemaName - the name of the schema to get
  232. * @returns {Promise} - returns promise (reject, resolve)
  233. */
  234. GET_SCHEMA(payload) {
  235. return new Promise(resolve => {
  236. resolve(this.schemas[payload.schemaName]);
  237. });
  238. }
  239. }
  240. export default new CacheModule();