Looks like eslint 4 adds `no-useless-escape` and `no-self-compare` to `eslint:recommended`.
166 lines
5.9 KiB
JavaScript
166 lines
5.9 KiB
JavaScript
'use strict';
|
|
|
|
// eslint-disable-next-line node/no-deprecated-api
|
|
const domain = require('domain');
|
|
const request = require('request');
|
|
const badge = require('./badge');
|
|
const {
|
|
makeBadgeData: getBadgeData
|
|
} = require('./badge-data');
|
|
const log = require('./log');
|
|
const LruCache = require('./lru-cache');
|
|
const {
|
|
incrMonthlyAnalytics,
|
|
getAnalytics
|
|
} = require('./analytics');
|
|
const {
|
|
makeSend
|
|
} = require('./result-sender');
|
|
|
|
// We avoid calling the vendor's server for computation of the information in a
|
|
// number of badges.
|
|
const minAccuracy = 0.75;
|
|
|
|
// The quotient of (vendor) data change frequency by badge request frequency
|
|
// must be lower than this to trigger sending the cached data *before*
|
|
// updating our data from the vendor's server.
|
|
// Indeed, the accuracy of our badges are:
|
|
// A(Δt) = 1 - min(# data change over Δt, # requests over Δt)
|
|
// / (# requests over Δt)
|
|
// = 1 - max(1, df) / rf
|
|
const freqRatioMax = 1 - minAccuracy;
|
|
|
|
// Request cache size of 5MB (~5000 bytes/image).
|
|
const requestCache = new LruCache(1000);
|
|
|
|
// Deep error handling for vendor hooks.
|
|
const vendorDomain = domain.create();
|
|
vendorDomain.on('error', function(err) {
|
|
log.error('Vendor hook error:', err.stack);
|
|
});
|
|
|
|
function handleRequest (vendorRequestHandler) {
|
|
return function getRequest(data, match, end, ask) {
|
|
if (data.maxAge !== undefined && /^[0-9]+$/.test(data.maxAge)) {
|
|
ask.res.setHeader('Cache-Control', 'max-age=' + data.maxAge);
|
|
} else {
|
|
// Cache management - no cache, so it won't be cached by GitHub's CDN.
|
|
ask.res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
}
|
|
const reqTime = new Date();
|
|
const date = (reqTime).toGMTString();
|
|
ask.res.setHeader('Expires', date); // Proxies, GitHub, see #221.
|
|
ask.res.setHeader('Date', date);
|
|
incrMonthlyAnalytics(getAnalytics().vendorMonthly);
|
|
if (data.style === 'flat') {
|
|
incrMonthlyAnalytics(getAnalytics().vendorFlatMonthly);
|
|
} else if (data.style === 'flat-square') {
|
|
incrMonthlyAnalytics(getAnalytics().vendorFlatSquareMonthly);
|
|
}
|
|
|
|
const cacheIndex = match[0] + '?label=' + data.label + '&style=' + data.style
|
|
+ '&logo=' + data.logo + '&logoWidth=' + data.logoWidth
|
|
+ '&link=' + JSON.stringify(data.link) + '&colorA=' + data.colorA
|
|
+ '&colorB=' + data.colorB;
|
|
// Should we return the data right away?
|
|
const cached = requestCache.get(cacheIndex);
|
|
let cachedVersionSent = false;
|
|
if (cached !== undefined) {
|
|
// A request was made not long ago.
|
|
const tooSoon = (+reqTime - cached.time) < cached.interval;
|
|
if (tooSoon || (cached.dataChange / cached.reqs <= freqRatioMax)) {
|
|
badge(cached.data.badgeData, makeSend(cached.data.format, ask.res, end));
|
|
cachedVersionSent = true;
|
|
// We do not wish to call the vendor servers.
|
|
if (tooSoon) { return; }
|
|
}
|
|
}
|
|
|
|
// In case our vendor servers are unresponsive.
|
|
let serverUnresponsive = false;
|
|
const serverResponsive = setTimeout(function() {
|
|
serverUnresponsive = true;
|
|
if (cachedVersionSent) { return; }
|
|
if (requestCache.has(cacheIndex)) {
|
|
const cached = requestCache.get(cacheIndex).data;
|
|
badge(cached.badgeData, makeSend(cached.format, ask.res, end));
|
|
return;
|
|
}
|
|
ask.res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
const badgeData = getBadgeData('vendor', data);
|
|
badgeData.text[1] = 'unresponsive';
|
|
let extension;
|
|
try {
|
|
extension = match[0].split('.').pop();
|
|
} catch(e) { extension = 'svg'; }
|
|
badge(badgeData, makeSend(extension, ask.res, end));
|
|
}, 25000);
|
|
|
|
// Only call vendor servers when last request is older than…
|
|
let cacheInterval = 5000; // milliseconds
|
|
function cachingRequest (uri, options, callback) {
|
|
if ((typeof options === 'function') && !callback) { callback = options; }
|
|
if (options && typeof options === 'object') {
|
|
options.uri = uri;
|
|
} else if (typeof uri === 'string') {
|
|
options = {uri: uri};
|
|
} else {
|
|
options = uri;
|
|
}
|
|
options.headers = options.headers || {};
|
|
options.headers['User-Agent'] = options.headers['User-Agent'] || 'Shields.io';
|
|
return request(options, function(err, res, body) {
|
|
if (res != null && res.headers != null) {
|
|
const cacheControl = res.headers['cache-control'];
|
|
if (cacheControl != null) {
|
|
const age = cacheControl.match(/max-age=([0-9]+)/);
|
|
// Would like to get some more test coverage on this before changing it.
|
|
// eslint-disable-next-line no-self-compare
|
|
if (age != null && (+age[1] === +age[1])) {
|
|
cacheInterval = +age[1] * 1000;
|
|
}
|
|
}
|
|
}
|
|
callback(err, res, body);
|
|
});
|
|
}
|
|
|
|
vendorDomain.run(function() {
|
|
vendorRequestHandler(data, match, function sendBadge(format, badgeData) {
|
|
if (serverUnresponsive) { return; }
|
|
clearTimeout(serverResponsive);
|
|
// Check for a change in the data.
|
|
let dataHasChanged = false;
|
|
if (cached !== undefined
|
|
&& cached.data.badgeData.text[1] !== badgeData.text[1]) {
|
|
dataHasChanged = true;
|
|
}
|
|
// Add format to badge data.
|
|
badgeData.format = format;
|
|
// Update information in the cache.
|
|
const updatedCache = {
|
|
reqs: cached? (cached.reqs + 1): 1,
|
|
dataChange: cached? (cached.dataChange + (dataHasChanged? 1: 0))
|
|
: 1,
|
|
time: +reqTime,
|
|
interval: cacheInterval,
|
|
data: { format: format, badgeData: badgeData }
|
|
};
|
|
requestCache.set(cacheIndex, updatedCache);
|
|
if (!cachedVersionSent) {
|
|
badge(badgeData, makeSend(format, ask.res, end));
|
|
}
|
|
}, cachingRequest);
|
|
});
|
|
};
|
|
}
|
|
|
|
function clearRequestCache() {
|
|
requestCache.clear();
|
|
}
|
|
|
|
module.exports = {
|
|
handleRequest,
|
|
clearRequestCache
|
|
};
|