foobot-graphql
Version:
GraphQL server for Foobot device data.
164 lines (137 loc) • 5.92 kB
JavaScript
;
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
};
}