UNPKG

foobot-graphql

Version:

GraphQL server for Foobot device data.

233 lines (201 loc) 10.3 kB
'use strict'; 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.'); }