lol-js
Version:
Node.js bindings for the Riot API, with caching and rate limiting
472 lines (443 loc) • 17.4 kB
JavaScript
// 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);