UNPKG

foobot-graphql

Version:

GraphQL server for Foobot device data.

164 lines (137 loc) 5.92 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _slicedToArray2 = require('babel-runtime/helpers/slicedToArray'); var _slicedToArray3 = _interopRequireDefault(_slicedToArray2); var _promise = require('babel-runtime/core-js/promise'); var _promise2 = _interopRequireDefault(_promise); exports.default = createLoaders; var _dataloader = require('dataloader'); var _dataloader2 = _interopRequireDefault(_dataloader); var _humanizeDuration = require('humanize-duration'); var _humanizeDuration2 = _interopRequireDefault(_humanizeDuration); var _expiringMap = require('./expiring-map'); var _expiringMap2 = _interopRequireDefault(_expiringMap); var _dataCache = require('./data-cache'); var _dataCache2 = _interopRequireDefault(_dataCache); var _limiter = require('./limiter'); var _limiter2 = _interopRequireDefault(_limiter); var _util = require('./util'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var debug = require('debug')('foobot-graphql:loaders'); var PERIOD_BUFFER = 10 * _util.ONE_MINUTE; function createLoaders(client) { var cache = new _dataCache2.default(); var limiter = new _limiter2.default(client); // Loads device metadata. var deviceLoader = new _dataloader2.default(function (keys) { return client.devices().then(function (devices) { var found = []; var notFound = []; keys.forEach(function (key) { var device = devices.find(function (device) { return device.uuid === key; }); device ? found.push(device) : notFound.push(key); }); if (notFound.length) { notFound.forEach(deviceLoader.clear, deviceLoader); throw new Error('Devices not found: ' + notFound.join(', ')); } return found; }); }, { cacheKeyFn: function cacheKeyFn(key) { return 'device=' + key; }, cacheMap: new _expiringMap2.default({ ttl: _util.ONE_DAY }) }); // Loads device datapoints and caches them until five minutes after the last // returned datapoint, when we expect new data to be available. var datapointsLoader = new _dataloader2.default(function (keys) { return _promise2.default.all(keys.map(function (key) { var _key = (0, _slicedToArray3.default)(key, 3), uuid = _key[0], period = _key[1], averageBy = _key[2]; return client.datapoints(uuid, { period: period, averageBy: averageBy }).then(function (data) { // Mark the data as expiring 5 minutes after the most recent datapoint. data.expires = data.end + 5 * 60; cache.add(uuid, data); return data; }); })); }, { cacheKeyFn: function cacheKeyFn(_ref) { var _ref2 = (0, _slicedToArray3.default)(_ref, 3), uuid = _ref2[0], period = _ref2[1], averageBy = _ref2[2]; return 'live: device=' + uuid + ' period=' + period + ' averageBy=' + averageBy; }, cacheMap: new _expiringMap2.default({ ttl: function ttl(key, value) { return value.then(function (data) { return data.expires == null ? 0 : (data.expires - data.date) * 1000; }).catch(function (err) { console.error(err); return 0; }); } }) }); // A wrapper around the `datapointsLoader` above which takes request limiting // into account, and uses a `DataCache` instance to perform averaging // ourselves. var cachedDatapointsLoader = new _dataloader2.default(function (keys) { return _promise2.default.all(keys.map(function (key) { var _key2 = (0, _slicedToArray3.default)(key, 3), uuid = _key2[0], period = _key2[1], averageBy = _key2[2]; debug('Incoming request. device=' + uuid + ' period=' + period + ' averageBy=' + averageBy); var now = new Date(); // If we don't have complete data for the requested period, then don't // rate limit the request, because the returned results would be wrong. var hasCompleteData = cache.hasCompleteData(uuid, period); if (!hasCompleteData) { debug('Incomplete data for the requested period.'); } var delay = hasCompleteData ? limiter.nextRequestDelay(now) : 0; // If a request is allowed in the next few seconds, don't retrieve cached // results, just wait. if (delay <= 3000) { debug('Requesting from API. delay=' + delay); return (0, _util.wait)(delay).then(function () { var data = cache.get(uuid, 0, 0); var requestPeriod = period; // If we already have data for some of the requested period, then we // might as well fetch a smaller window of data to speed things up // and play nice with the API. if (data.end != null) { var lastTime = data.end * 1000; // Fetch the period between now and the last known datapoint, // expanded by a buffer zone of ten minutes to make sure we don't // miss any due to clock skew, and rounded up to the nearest hour. requestPeriod = now.getTime() - lastTime + PERIOD_BUFFER; requestPeriod = _util.ONE_HOUR * Math.ceil(requestPeriod / _util.ONE_HOUR); requestPeriod /= 1000; requestPeriod = Math.min(requestPeriod, period); } requestPeriod = Math.floor(requestPeriod); // Integer seconds. return datapointsLoader.load([uuid, requestPeriod, 0]).then(function (data) { return cache.get(uuid, period, averageBy); }); }); } else { debug('Next request delayed, retrieving from cache. ' + ('delay=\'' + (0, _humanizeDuration2.default)(delay) + '\'')); return cache.get(uuid, period, averageBy); } })); }, { cache: false }); return { device: deviceLoader, datapoints: cachedDatapointsLoader }; }