core.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import async from "async";
  2. import config from "config";
  3. class DeferredPromise {
  4. // eslint-disable-next-line require-jsdoc
  5. constructor() {
  6. this.promise = new Promise((resolve, reject) => {
  7. this.reject = reject;
  8. this.resolve = resolve;
  9. });
  10. }
  11. }
  12. class MovingAverageCalculator {
  13. // eslint-disable-next-line require-jsdoc
  14. constructor() {
  15. this.count = 0;
  16. this._mean = 0;
  17. }
  18. /**
  19. * Updates the mean average
  20. *
  21. * @param {number} newValue - the new time it took to complete a job
  22. */
  23. update(newValue) {
  24. this.count += 1;
  25. const differential = (newValue - this._mean) / this.count;
  26. this._mean += differential;
  27. }
  28. /**
  29. * Returns the mean average
  30. *
  31. * @returns {number} - returns the mean average
  32. */
  33. get mean() {
  34. this.validate();
  35. return this._mean;
  36. }
  37. /**
  38. * Checks that the mean is valid
  39. */
  40. validate() {
  41. if (this.count === 0) throw new Error("Mean is undefined");
  42. }
  43. }
  44. export default class CoreClass {
  45. // eslint-disable-next-line require-jsdoc
  46. constructor(name) {
  47. this.name = name;
  48. this.status = "UNINITIALIZED";
  49. // this.log("Core constructor");
  50. this.jobQueue = async.priorityQueue(
  51. ({ job, options }, callback) => this._runJob(job, options, callback),
  52. 10 // How many jobs can run concurrently
  53. );
  54. this.jobQueue.pause();
  55. this.runningJobs = [];
  56. this.priorities = {};
  57. this.stage = 0;
  58. this.jobStatistics = {};
  59. this.registerJobs();
  60. }
  61. /**
  62. * Sets the status of a module
  63. *
  64. * @param {string} status - the new status of a module
  65. */
  66. setStatus(status) {
  67. this.status = status;
  68. this.log("INFO", `Status changed to: ${status}`);
  69. if (this.status === "READY") this.jobQueue.resume();
  70. else if (this.status === "FAIL" || this.status === "LOCKDOWN") this.jobQueue.pause();
  71. }
  72. /**
  73. * Returns the status of a module
  74. *
  75. * @returns {string} - the status of a module
  76. */
  77. getStatus() {
  78. return this.status;
  79. }
  80. /**
  81. * Changes the current stage of a module
  82. *
  83. * @param {string} stage - the new stage of a module
  84. */
  85. setStage(stage) {
  86. this.stage = stage;
  87. }
  88. /**
  89. * Returns the current stage of a module
  90. *
  91. * @returns {string} - the current stage of a module
  92. */
  93. getStage() {
  94. return this.stage;
  95. }
  96. /**
  97. * Initialises a module and handles initialise successes and failures
  98. */
  99. _initialize() {
  100. this.setStatus("INITIALIZING");
  101. this.initialize()
  102. .then(() => {
  103. this.setStatus("READY");
  104. this.moduleManager.onInitialize(this);
  105. })
  106. .catch(err => {
  107. console.error(err);
  108. this.setStatus("FAILED");
  109. this.moduleManager.onFail(this);
  110. });
  111. }
  112. /**
  113. * Creates a new log message
  114. *
  115. * @param {...any} args - anything to be included in the log message, the first argument is the type of log
  116. */
  117. log(...args) {
  118. const _arguments = Array.from(args);
  119. const type = _arguments[0];
  120. if (config.debug && config.debug.stationIssue === true && type === "STATION_ISSUE") {
  121. this.moduleManager.debugLogs.stationIssue.push(_arguments);
  122. return;
  123. }
  124. _arguments.splice(0, 1);
  125. const start = `|${this.name.toUpperCase()}|`;
  126. const numberOfSpacesNeeded = 20 - start.length;
  127. _arguments.unshift(`${start}${Array(numberOfSpacesNeeded).join(" ")}`);
  128. if (type === "INFO") {
  129. _arguments[0] += "\x1b[36m";
  130. _arguments.push("\x1b[0m");
  131. console.log.apply(null, _arguments);
  132. } else if (type === "ERROR") {
  133. _arguments[0] += "\x1b[31m";
  134. _arguments.push("\x1b[0m");
  135. console.error.apply(null, _arguments);
  136. }
  137. }
  138. /**
  139. * Sets up each job with the statistics service (includes mean average for job completion)
  140. */
  141. registerJobs() {
  142. let props = [];
  143. let obj = this;
  144. do {
  145. props = props.concat(Object.getOwnPropertyNames(obj));
  146. // eslint-disable-next-line no-cond-assign
  147. } while ((obj = Object.getPrototypeOf(obj)));
  148. const jobNames = props.sort().filter(prop => typeof this[prop] === "function" && prop === prop.toUpperCase());
  149. jobNames.forEach(jobName => {
  150. this.jobStatistics[jobName] = {
  151. successful: 0,
  152. failed: 0,
  153. total: 0,
  154. averageTiming: new MovingAverageCalculator()
  155. };
  156. });
  157. }
  158. /**
  159. * Runs a job
  160. *
  161. * @param {string} name - the name of the job e.g. GET_PLAYLIST
  162. * @param {object} payload - any expected payload for the job itself
  163. * @param {object} options - object containing any additional options for the job
  164. * @param {boolean} options.isQuiet - whether or not the job should be advertised in the logs, useful for repetitive/unimportant jobs
  165. * @param {boolean} options.bypassQueue - UNKNOWN
  166. * @returns {Promise} - returns a promise
  167. */
  168. runJob(name, payload, options = { isQuiet: false, bypassQueue: false }) {
  169. const deferredPromise = new DeferredPromise();
  170. const job = { name, payload, onFinish: deferredPromise };
  171. if (
  172. config.debug &&
  173. config.debug.stationIssue === true &&
  174. config.debug.captureJobs &&
  175. config.debug.captureJobs.indexOf(name) !== -1
  176. ) {
  177. this.moduleManager.debugJobs.all.push(job);
  178. }
  179. if (options.bypassQueue) this._runJob(job, options, () => { });
  180. else {
  181. const priority = this.priorities[name] ? this.priorities[name] : 10;
  182. this.jobQueue.push({ job, options }, priority);
  183. }
  184. return deferredPromise.promise;
  185. }
  186. /**
  187. * UNKNOWN
  188. *
  189. * @param {object} moduleManager - UNKNOWN
  190. */
  191. setModuleManager(moduleManager) {
  192. this.moduleManager = moduleManager;
  193. }
  194. /**
  195. * Actually runs the job? UNKNOWN
  196. *
  197. * @param {object} job - object containing details of the job
  198. * @param {string} job.name - the name of the job e.g. GET_PLAYLIST
  199. * @param {string} job.payload - any expected payload for the job itself
  200. * @param {Promise} job.onFinish - deferred promise when the job is complete
  201. * @param {object} options - object containing any additional options for the job
  202. * @param {boolean} options.isQuiet - whether or not the job should be advertised in the logs, useful for repetitive/unimportant jobs
  203. * @param {boolean} options.bypassQueue - UNKNOWN
  204. * @param {Function} cb - Callback after the job has completed
  205. */
  206. _runJob(job, options, cb) {
  207. if (!options.isQuiet) this.log("INFO", `Running job ${job.name}`);
  208. const startTime = Date.now();
  209. this.runningJobs.push(job);
  210. const newThis = Object.assign(Object.create(Object.getPrototypeOf(this)), this);
  211. newThis.runJob = (...args) => {
  212. if (args.length === 1) args.push({});
  213. args[1].bypassQueue = true;
  214. return this.runJob(...args);
  215. };
  216. this[job.name]
  217. .apply(newThis, [job.payload])
  218. .then(response => {
  219. if (!options.isQuiet) this.log("INFO", `Ran job ${job.name} successfully`);
  220. this.jobStatistics[job.name].successful += 1;
  221. if (
  222. config.debug &&
  223. config.debug.stationIssue === true &&
  224. config.debug.captureJobs &&
  225. config.debug.captureJobs.indexOf(job.name) !== -1
  226. ) {
  227. this.moduleManager.debugJobs.completed.push({
  228. status: "success",
  229. job,
  230. response
  231. });
  232. }
  233. job.onFinish.resolve(response);
  234. })
  235. .catch(error => {
  236. this.log("INFO", `Running job ${job.name} failed`);
  237. this.jobStatistics[job.name].failed += 1;
  238. if (
  239. config.debug &&
  240. config.debug.stationIssue === true &&
  241. config.debug.captureJobs &&
  242. config.debug.captureJobs.indexOf(job.name) !== -1
  243. ) {
  244. this.moduleManager.debugJobs.completed.push({
  245. status: "error",
  246. job,
  247. error
  248. });
  249. }
  250. job.onFinish.reject(error);
  251. })
  252. .finally(() => {
  253. const endTime = Date.now();
  254. const executionTime = endTime - startTime;
  255. this.jobStatistics[job.name].total += 1;
  256. this.jobStatistics[job.name].averageTiming.update(executionTime);
  257. this.runningJobs.splice(this.runningJobs.indexOf(job), 1);
  258. cb();
  259. });
  260. }
  261. }