'use strict'; const EventEmitter = require('events'); const retryCodes = [429].concat((process.env.JSON_CACHE_RETRY_CODES || '') .split(',').map(code => parseInt(code.trim(), 10))); const defaultOpts = { parser: JSON.parse, promiseLib: Promise, logger: console, delayStart: false, opts: {}, maxListeners: 10, useEmitter: false, maxRetry: 1, integrity: () => true, }; class JSONCache extends EventEmitter { /** * Make a new cache * @param {string} url url to fetch * @param {number} [timeout=60000] optional timeout * @param {Object} options Options object * @param {function} options.parser optional parser to parse data. defaults to JSON.parse * @param {Class} options.promiseLib optional promise library override * @param {Object} options.logger optional Logger * @param {boolean} options.delayStart whether or not to delay starting updating the cache * until start is requested * @param {Object} options.opts options to pass to the parser * @param {number} options.maxListeners maximum listeners * (only applicable if leveraging emitter) * @param {boolean} options.useEmitter whether or not to use the optional node emitter * @param {number} options.maxRetry maximum number of attempts to retry getting data * @param {function} options.integrity optional function to check if the data is worth keeping */ constructor(url, timeout, options) { super(); // eslint-disable-next-line no-param-reassign options = { ...defaultOpts, ...options, }; const { parser, promiseLib, logger, delayStart, opts, maxListeners, useEmitter, maxRetry, integrity, } = options; this.url = url; // eslint-disable-next-line global-require this.protocol = this.url.startsWith('https') ? require('https') : require('http'); this.maxRetry = maxRetry; this.timeout = timeout || 60000; this.currentData = null; this.updating = null; this.Promise = promiseLib; this.parser = parser; this.hash = null; this.logger = logger; this.delayStart = delayStart; this.opts = opts; this.useEmitter = useEmitter; this.integrity = integrity; if (useEmitter) { this.setMaxListeners(maxListeners); } if (!delayStart) { this.startUpdating(); } } getData() { if (this.delayStart && !this.currentData && !this.updating) { this.startUpdating(); } if (this.updating) { return this.updating; } return this.Promise.resolve(this.currentData); } getDataJson() { return this.getData(); } update() { this.updating = this.httpGet().then(async (data) => { const parsed = this.parser(data, this.opts); if (!this.integrity(parsed)) return this.currentData; // data passed integrity check this.currentData = parsed; if (this.useEmitter) { setTimeout(async () => this.emit('update', await this.currentData), 2000); } this.updating = null; return this.currentData; }).catch((err) => { this.updating = null; throw err; }); } httpGet() { return new this.Promise((resolve) => { const request = this.protocol.get(this.url, (response) => { this.logger.debug(`beginning request to ${this.url}`); const body = []; if (response.statusCode < 200 || response.statusCode > 299) { if ((response.statusCode > 499 || retryCodes.includes(response.statusCode)) && this.retryCount < this.maxRetry) { this.retryCount += 1; setTimeout(() => this.httpGet().then(resolve).catch(this.logger.error), 1000); } else { this.logger.error(`${response.statusCode}: Failed to load ${this.url}`); resolve('[]'); } } else { response.on('data', chunk => body.push(chunk)); response.on('end', () => { this.retryCount = 0; resolve(body.join('')); }); } }); request.on('error', (err) => { this.logger.error(`${err.statusCode}: ${this.url}`); resolve('[]'); }); }); } startUpdating() { this.updateInterval = setInterval(() => this.update(), this.timeout); this.update(); } stop() { clearInterval(this.updateInterval); } stopUpdating() { this.stop(); } } module.exports = JSONCache;