UNPKG

lol-js

Version:

Node.js bindings for the Riot API, with caching and rate limiting

472 lines (443 loc) 17.4 kB
// Generated by CoffeeScript 1.9.2 (function() { var Client, EventEmitter, MAX_RETRIES_ON_RIOT_API_UNAVAILABLE, ONE_MONTH_IN_SECONDS, Promise, RateLimiter, TIME_TO_WAIT_FOR_RIOT_API_IN_MS, fs, ld, path, promiseTools, querystring, utils, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; EventEmitter = require('events').EventEmitter; querystring = require('querystring'); ld = require('lodash'); fs = require('fs'); path = require('path'); Promise = require('es6-promise').Promise; promiseTools = require('promise-tools'); utils = require('./utils'); RateLimiter = require('./rateLimiter'); MAX_RETRIES_ON_RIOT_API_UNAVAILABLE = 10; TIME_TO_WAIT_FOR_RIOT_API_IN_MS = 100; ONE_MONTH_IN_SECONDS = 30 * 24 * 60 * 60; module.exports = Client = (function(superClass) { extend(Client, superClass); Client.prototype.constants = require('./constants'); function Client(options) { var rateLimitOptions, ref, ref1; if (options == null) { options = {}; } this.Promise = (ref = options.Promise) != null ? ref : Promise; if (options.apiKey == null) { throw new Error('apiKey is required.'); } this.apiKey = options.apiKey; this.cacheTTL = ld.defaults({}, options.cacheTTL, { short: 60 * 5, long: ONE_MONTH_IN_SECONDS, flex: ONE_MONTH_IN_SECONDS }); if (options.cache != null) { this.cache = { get: (function(_this) { return function(params) { return new _this.Promise(function(resolve, reject) { var answer; return answer = options.cache.get(params, function(err, answer) { if (err != null) { _this._stats.errors++; _this.emit('cacheGetError', err); return resolve(null); } if (answer == null) { _this._stats.misses++; } else { _this._stats.hits++; if (answer.cacheTime != null) { resolve(answer); } else { if (answer === 'none') { answer = null; } resolve({ value: answer, cacheTime: 0, ttl: 0 }); } } return resolve(answer); }); }); }; })(this), set: (function(_this) { return function(params, value) { var cacheTime, cacheValue, err, ref1, ttl; try { cacheTime = Date.now(); ttl = (ref1 = params.ttl) != null ? ref1 : _this.cacheTTL.short; cacheValue = { value: value, cacheTime: cacheTime, ttl: ttl }; if (ttl < _this.cacheTTL.flex) { ttl = _this.cacheTTL.flex; } if (params.ttl == null) { params = ld.extend({}, params, { ttl: ttl }); } return options.cache.set(params, cacheValue); } catch (_error) { err = _error; _this._stats.errors++; return _this.emit('cacheSetError', err); } }; })(this), destroy: function() { var base; return typeof (base = options.cache).destroy === "function" ? base.destroy() : void 0; } }; } else { this.cache = { get: (function(_this) { return function(params) { return new _this.Promise(function(resolve, reject) { return resolve(null); }); }; })(this), set: function() {}, destroy: function() {} }; } rateLimitOptions = (ref1 = options.rateLimit) != null ? ref1 : [ { time: 10, limit: 10 }, { time: 600, limit: 500 } ]; this._rateLimiter = new RateLimiter(rateLimitOptions, this.Promise); this._queuedRequests = []; this._outstandingRequests = {}; this._processingRequests = false; this._stats = { hits: 0, misses: 0, errors: 0, rateLimitErrors: 0, queueHighWaterMark: 0, riotApiUnavailable: 0 }; this._request = require('request'); } Client.prototype.destroy = function() { return this.cache.destroy(); }; Client.prototype.getStats = function() { return ld.merge({}, this._stats, { queueLength: this._queuedRequests.length }); }; Client.prototype._doRequest = function(params) { var allowRetries, caller, ref, retries, url; if (params.retries == null) { params.retries = 0; } url = params.url, caller = params.caller, retries = params.retries; allowRetries = (ref = params.allowRetries) != null ? ref : true; return new this.Promise((function(_this) { return function(resolve, reject) { return _this._request({ uri: url, gzip: true }, function(err, response, body) { try { if (err != null) { return reject(err); } if (response.statusCode === 429) { _this._stats.rateLimitErrors++; params.retries = 0; return _this._rateLimiter.wait().then(function() { return _this._doRequest(params).then(resolve, reject); }); } else if (response.statusCode === 404) { return resolve(null); } else if (response.statusCode === 503) { _this._riotApiUnavailable++; if (allowRetries && retries < MAX_RETRIES_ON_RIOT_API_UNAVAILABLE) { return promiseTools.delay(TIME_TO_WAIT_FOR_RIOT_API_IN_MS).then(function() { return _this._rateLimiter.wait(); }).then(function() { params.retries++; return _this._doRequest(params).then(resolve, reject); })["catch"](reject); } else { err = new Error("Riot API is temporarily unavailable"); err.statusCode = response.statusCode; err.caller = caller; return reject(err); } } else if (response.statusCode !== 200) { err = new Error("Error calling " + params.caller + ": " + response.statusCode); err.statusCode = response.statusCode; err.caller = caller; return reject(err); } else { return resolve(JSON.parse(body)); } } catch (_error) { err = _error; return reject(err); } }); }; })(this)); }; Client.prototype._startRequestWorker = function() { var doWork; if (this._processingRequests) { return; } this._processingRequests = true; doWork = (function(_this) { return function() { if (_this._queuedRequests.length === 0) { return _this._processingRequests = false; } else { return _this._rateLimiter.wait().then(function() { var allowRetries, caller, ref, reject, requestId, resolve, url; setImmediate(doWork); ref = _this._queuedRequests.shift(), url = ref.url, caller = ref.caller, allowRetries = ref.allowRetries, resolve = ref.resolve, reject = ref.reject, requestId = ref.requestId; return _this._doRequest({ url: url, caller: caller, allowRetries: allowRetries }).then(resolve, reject).then(function(result) { delete _this._outstandingRequests[requestId]; return result; }, function(err) { delete _this._outstandingRequests[requestId]; throw err; }); })["catch"](function(err) { var ref; console.err("Fatal error in lol-js worker"); console.err((ref = err.stack) != null ? ref : err); return process.exit(-1); }); } }; })(this); return setImmediate(doWork); }; Client.prototype._riotRequest = function(params, haveCached) { var answer, caller, existingRequest, promise, queryParams, queryString, queueItem, ref, ref1, reject, requestId, resolve, url; if (params.queryParams) { queryParams = ld(params.queryParams).map(function(v, k) { return [k, v]; }).sortBy(0).zipObject().value(); queryString = querystring.stringify(queryParams); } else { queryString = null; } requestId = params.url + "?" + (queryString != null ? queryString : ''); url = "" + requestId + (queryString != null ? '&' : '') + "api_key=" + this.apiKey; caller = params.caller; if (!((ref = params.rateLimit) != null ? ref : true)) { answer = this._doRequest({ url: url, caller: caller, allowRetries: !haveCached }); } else if ((existingRequest = this._outstandingRequests[requestId]) != null) { answer = existingRequest.promise; } else { ref1 = promiseTools.defer(), promise = ref1.promise, resolve = ref1.resolve, reject = ref1.reject; queueItem = { requestId: requestId, url: url, caller: caller, promise: promise, resolve: resolve, reject: reject, allowRetries: !haveCached }; this._queuedRequests.push(queueItem); this._outstandingRequests[requestId] = queueItem; this._stats.queueHighWaterMark = Math.max(this._stats.queueHighWaterMark, this._queuedRequests.length); this._startRequestWorker(); answer = promise; } return answer; }; Client.prototype._validateCacheParams = function(cacheParams) { var j, key, len, ref; ref = ['key', 'api', 'objectType', 'region', 'params']; for (j = 0, len = ref.length; j < len; j++) { key = ref[j]; if (!(key in cacheParams)) { throw new Error("Missing " + key + " in cacheParams."); } } if (!'ttl' in cacheParams) { return cacheParams.ttl != null ? cacheParams.ttl : cacheParams.ttl = this.cacheTTL.short; } }; Client.prototype._riotRequestWithCache = function(params, cacheParams, options) { var cachedAnswer; if (options == null) { options = {}; } this._validateCacheParams(cacheParams); cachedAnswer = null; return this.cache.get(cacheParams).then((function(_this) { return function(cachedAnswer) { var answer, cacheTime, expires, ref, ttl, value; ref = cachedAnswer != null ? cachedAnswer : {}, value = ref.value, cacheTime = ref.cacheTime, ttl = ref.ttl; if (ttl == null) { ttl = 0; } expires = (cacheTime != null ? cacheTime : 0) + (ttl * 1000); if ((cachedAnswer == null) || (expires < Date.now())) { answer = _this._riotRequest(params, cachedAnswer != null).then(function(result) { if (options.preCache != null) { return options.preCache(result); } else { return result; } }).then(function(result) { _this.cache.set(cacheParams, result); return result; })["catch"](function(err) { if (cachedAnswer != null) { return cachedAnswer.value; } else { throw err; } }); } else { answer = cachedAnswer.value; } return answer; }; })(this)); }; Client.prototype._riotMultiGet = function(region, params) { var answer, baseUrl, cacheResultFn, caller, getCacheParamsFn, ids, maxObjs, queryParams, urlSuffix; caller = params.caller, baseUrl = params.baseUrl, ids = params.ids, urlSuffix = params.urlSuffix, getCacheParamsFn = params.getCacheParamsFn, cacheResultFn = params.cacheResultFn, queryParams = params.queryParams, maxObjs = params.maxObjs; if (!ld.isArray(ids)) { ids = [ids]; } answer = {}; return this.Promise.all(ids.map((function(_this) { return function(id) { return new _this.Promise(function(resolve, reject) { var cacheParams; cacheParams = getCacheParamsFn(_this, region, id); _this._validateCacheParams(cacheParams); return _this.cache.get(cacheParams).then(function(object) { return resolve({ id: id, cacheParams: cacheParams, object: object }); }, reject); }); }; })(this))).then((function(_this) { return function(objects) { var cacheParams, groups, i, id, j, len, missingObjects, object, ref; missingObjects = []; for (j = 0, len = objects.length; j < len; j++) { ref = objects[j], id = ref.id, cacheParams = ref.cacheParams, object = ref.object; if ((object == null) || (((object != null ? object.expires : void 0) != null) && object.expires < Date.now())) { missingObjects.push({ id: id, cacheParams: cacheParams, cached: object }); } else { answer[id] = object.value; } } if (missingObjects.length === 0) { return answer; } else { groups = (function() { var l, ref1, results; results = []; for (i = l = 0, ref1 = Math.ceil(missingObjects.length / maxObjs); 0 <= ref1 ? l < ref1 : l > ref1; i = 0 <= ref1 ? ++l : --l) { results.push(missingObjects.slice(i * maxObjs, i * maxObjs + maxObjs)); } return results; })(); return _this.Promise.all(groups.map(function(group) { var haveCached, urlIds; haveCached = ld.every(group, function(g) { return g.cached != null; }); urlIds = group.map(function(item) { return encodeURIComponent(item.id); }).join(','); return _this._riotRequest({ caller: caller, region: region, url: baseUrl + "/" + urlIds + (urlSuffix != null ? urlSuffix : ''), queryParams: queryParams }, haveCached).then(function(fetchedObjects) { var l, len1, ref1, ref2; if (fetchedObjects == null) { fetchedObjects = {}; } for (l = 0, len1 = group.length; l < len1; l++) { ref1 = group[l], id = ref1.id, cacheParams = ref1.cacheParams; answer[id] = (ref2 = fetchedObjects[id]) != null ? ref2 : null; if ((answer[id] != null) && (typeof cacheResultsFn !== "undefined" && cacheResultsFn !== null)) { cacheResultFn(_this, region, answer[id]); } else { _this.cache.set(cacheParams, answer[id]); } } return null; })["catch"](function(err) { var cached, l, len1, ref1; for (l = 0, len1 = group.length; l < len1; l++) { ref1 = group[l], id = ref1.id, cached = ref1.cached; if (cached == null) { throw err; } answer[id] = cached.value; } return null; }); })); } }; })(this)).then(function() { return answer; }); }; Client.prototype._makeUrl = function(region, api) { return "https://" + region + ".api.pvp.net/api/lol/" + region + "/" + api.version + "/" + api.name; }; return Client; })(EventEmitter); (function() { var api, j, len, moduleFile, moduleName, ref, results; ref = fs.readdirSync(path.join(__dirname, "api")); results = []; for (j = 0, len = ref.length; j < len; j++) { moduleFile = ref[j]; moduleName = path.basename(moduleFile, path.extname(moduleFile)); api = require("./api/" + moduleName); results.push(ld.extend(Client.prototype, api.methods)); } return results; })(); }).call(this);