foobot-graphql
Version:
GraphQL server for Foobot device data.
233 lines (201 loc) • 10.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _createClass2 = require('babel-runtime/helpers/createClass');
var _createClass3 = _interopRequireDefault(_createClass2);
var _dateFns = require('date-fns');
var _dateFns2 = _interopRequireDefault(_dateFns);
var _humanizeDuration = require('humanize-duration');
var _humanizeDuration2 = _interopRequireDefault(_humanizeDuration);
var _util = require('./util');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var debug = require('debug')('foobot-graphql:limiter'); /**
* The Foobot API is rate limited. This class helps us figure out when we're
* allowed to make another request.
*
* In the worst case, consumers of this GraphQL service will want up-to-date
* results for the entire day -- for example, by keeping open a dashboard that
* is constantly refreshed with the most recent results. The API updates every
* five minutes, but there's a problem: we're limited to 200 API requests per
* day, yet there are 288 five-minute intervals in a day. So unless you MITM
* the Foobot app and steal their unlimited API key (which works, but is
* troublesome), we need to figure out how to make fewer API calls while
* staying reasonably up to date.
*
* Limiter's job is to schedule spaced out fetches. The fetch delay is
* determined based on the time of day and the number of remaining API requests
* we think we have based on the headers returned in the previous response.
* Note that all of the day's datapoints are still fetched no matter what (by
* expanding the requested period). So there will never be any dropped
* datapoints, we just might not fetch new ones right away.
*
* Run this file as a standalone script to test the delay parameters.
*/
var MAX_DISTANCE = _util.ONE_DAY / 2;
// TODO: There is unused code here to allow consumers to specify a "peak time"
// during which we'll make requests at 5-minute intervals no matter what, and
// gradually make fewer requests the farther we get from that time. Currently,
// we just evenly space out the day's remaining requests.
var Limiter = function () {
function Limiter(client) {
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref$peakTimeUTC = _ref.peakTimeUTC,
peakTimeUTC = _ref$peakTimeUTC === undefined ? _util.ONE_HOUR / 2 : _ref$peakTimeUTC,
_ref$resetTimeUTC = _ref.resetTimeUTC,
resetTimeUTC = _ref$resetTimeUTC === undefined ? 0 : _ref$resetTimeUTC,
_ref$limitTarget = _ref.limitTarget,
limitTarget = _ref$limitTarget === undefined ? 200 : _ref$limitTarget;
(0, _classCallCheck3.default)(this, Limiter);
this.client = client;
this.peakTimeUTC = peakTimeUTC;
this.resetTimeUTC = resetTimeUTC;
this.limitTarget = limitTarget;
}
(0, _createClass3.default)(Limiter, [{
key: 'distanceFromPeak',
value: function distanceFromPeak() {
var date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Date();
var zero = new Date(date.getTime());
zero.setUTCHours(0);
zero.setUTCMinutes(0);
zero.setUTCSeconds(0);
zero.setUTCMilliseconds(0);
var distanceFromZero = date.getTime() - zero.getTime();
return Math.min(Math.abs(distanceFromZero - this.peakTimeUTC), Math.abs(distanceFromZero - this.peakTimeUTC + _util.ONE_DAY), Math.abs(distanceFromZero - this.peakTimeUTC - _util.ONE_DAY));
}
}, {
key: 'delayAtDistance',
value: function delayAtDistance(distance) {
var base = 5 * _util.ONE_MINUTE;
var increase = 1000 * Math.exp(8.15 * distance / MAX_DISTANCE);
var delay = base + increase;
return 1000 * Math.floor(delay / 1000); // Round down to second.
}
}, {
key: 'delayAtTime',
value: function delayAtTime() {
var date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Date();
return this.delayAtDistance(this.distanceFromPeak(date));
}
}, {
key: 'nextResetTime',
value: function nextResetTime() {
var date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Date();
var resetTime = new Date(date.getTime());
resetTime.setUTCHours(0);
resetTime.setUTCMinutes(0);
resetTime.setUTCSeconds(0);
resetTime.setUTCMilliseconds(this.resetTimeUTC);
while (resetTime.getTime() <= date.getTime()) {
resetTime.setUTCDate(resetTime.getUTCDate() + 1);
}
return resetTime;
}
}, {
key: 'distanceFromReset',
value: function distanceFromReset() {
var date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Date();
var resetTime = this.nextResetTime(date);
return resetTime.getTime() - date.getTime();
}
}, {
key: 'delayAtLimit',
value: function delayAtLimit(lastRequestTime, lastRequestLimit) {
var date = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : new Date();
if (lastRequestTime == null || lastRequestLimit == null) {
return 0;
}
var wasReset = this.nextResetTime(lastRequestTime) <= date;
var limit = wasReset ? this.limitTarget : Math.min(this.limitTarget, lastRequestLimit);
var distance = this.distanceFromReset(date);
var avgDelay = distance / limit;
/*
const base = minDelay
const factor = 300 // Magic number to get the desired distribution.
const increase = 500 * Math.exp(factor * avgDelay / maxDelay)
*/
var delay = Math.min(distance, avgDelay);
return 1000 * Math.floor(delay / 1000); // Round down to second.
}
}, {
key: 'nextRequestTime',
value: function nextRequestTime() {
var date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Date();
var nextRequestTime = date;
if (this.client.lastRequestTime == null) {
debug('No previous request.');
} else if (this.client.lastRequestLimit === 0) {
nextRequestTime = this.nextResetTime(this.client.lastRequestTime);
debug('Request limit reached, delaying until next reset.');
} else {
var delay = this.delayAtLimit(this.client.lastRequestTime, this.client.lastRequestLimit, date);
nextRequestTime = new Date(this.client.lastRequestTime.getTime() + delay);
debug('Delaying based on previous request.');
}
return nextRequestTime;
}
}, {
key: 'nextRequestDelay',
value: function nextRequestDelay() {
var date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Date();
var nextTime = this.nextRequestTime(date);
var delay = 0;
if (nextTime > date) {
delay = nextTime.getTime() - date.getTime();
}
debug('Next request can be made %s.', delay ? 'in ' + (0, _humanizeDuration2.default)(delay) : 'now');
return delay;
}
}, {
key: 'canMakeRequest',
value: function canMakeRequest() {
var date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Date();
return date >= this.nextRequestTime(date);
}
}]);
return Limiter;
}();
exports.default = Limiter;
if (require.main === module) {
var client = { lastRequestTime: null, lastRequestLimit: 50 };
var limiter = new Limiter(client, { resetTime: 0 });
// Check how the delay changes throughout the day.
var now = new Date();
for (var i = 0; i < 48; i++) {
var _time = new Date(now.getFullYear(), now.getMonth(), now.getDate());
_time.setMilliseconds(i * _util.ONE_HOUR / 2);
client.lastRequestTime = _time;
client.lastRequestLimit -= 1;
var delay = limiter.delayAtLimit(client.lastRequestTime, client.lastRequestLimit, _time);
console.log('hour: ' + _dateFns2.default.format(_time, 'HH:mm') + ', ' + ('limit: ' + client.lastRequestLimit + ', ') + ('delay: ' + (0, _humanizeDuration2.default)(delay)));
}
console.log();
// Check the maximum number of ticks that will occur under ideal conditions.
var sum = 0;
var count = 0;
var time = new Date(2016, 8, 15);
time.setUTCHours(0);
time.setUTCMinutes(0);
time.setUTCSeconds(0);
time.setUTCMilliseconds(0);
client.lastRequestTime = null;
client.lastRequestLimit = 200;
while (sum < _util.ONE_DAY) {
client.lastRequestTime = time;
client.lastRequestLimit -= 1;
var _delay = limiter.nextRequestDelay(time);
console.log('time: ' + _dateFns2.default.format(time, 'HH:mm') + ', ' + ('limit: ' + client.lastRequestLimit + ', ') + ('delay: ' + (0, _humanizeDuration2.default)(_delay)));
sum += _delay;
count += 1;
var lastTime = time;
var nextResetTime = limiter.nextResetTime(lastTime);
time = new Date(time.getTime() + _delay);
if (time >= nextResetTime) {
client.lastRequestLimit = 200;
}
}
console.log('Current settings will result in ~' + count + ' API calls per day.');
}