Files
shields/services/github/github-api-provider.js
2019-01-18 06:18:31 -05:00

162 lines
4.0 KiB
JavaScript

'use strict'
const Joi = require('joi')
const { TokenPool } = require('../../core/token-pooling/token-pool')
const { nonNegativeInteger } = require('../validators')
const headerSchema = Joi.object({
'x-ratelimit-limit': nonNegativeInteger,
'x-ratelimit-remaining': nonNegativeInteger,
'x-ratelimit-reset': nonNegativeInteger,
})
.required()
.unknown(true)
// Provides an interface to the Github API. Manages the base URL.
class GithubApiProvider {
// reserveFraction: The amount of much of a token's quota we avoid using, to
// reserve it for the user.
constructor({
baseUrl,
withPooling = true,
onTokenInvalidated = tokenString => {},
globalToken,
reserveFraction = 0.25,
}) {
Object.assign(this, {
baseUrl,
withPooling,
onTokenInvalidated,
globalToken,
reserveFraction,
})
if (this.withPooling) {
this.standardTokens = new TokenPool({ batchSize: 25 })
this.searchTokens = new TokenPool({ batchSize: 5 })
}
}
serializeDebugInfo({ sanitize = true } = {}) {
if (this.withPooling) {
return {
standardTokens: this.standardTokens.serializeDebugInfo({ sanitize }),
searchTokens: this.searchTokens.serializeDebugInfo({ sanitize }),
}
} else {
return {}
}
}
addToken(tokenString) {
if (this.withPooling) {
this.standardTokens.add(tokenString)
this.searchTokens.add(tokenString)
} else {
throw Error('When not using a token pool, do not provide tokens')
}
}
updateToken(token, headers) {
let rateLimit, totalUsesRemaining, nextReset
try {
;({
'x-ratelimit-limit': rateLimit,
'x-ratelimit-remaining': totalUsesRemaining,
'x-ratelimit-reset': nextReset,
} = Joi.attempt(headers, headerSchema))
} catch (e) {
const logHeaders = {
'x-ratelimit-limit': headers['x-ratelimit-limit'],
'x-ratelimit-remaining': headers['x-ratelimit-remaining'],
'x-ratelimit-reset': headers['x-ratelimit-reset'],
}
console.log(
`Invalid GitHub rate limit headers ${JSON.stringify(
logHeaders,
undefined,
2
)}`
)
return
}
const reserve = Math.ceil(this.reserveFraction * rateLimit)
const usesRemaining = totalUsesRemaining - reserve
token.update(usesRemaining, nextReset)
}
invalidateToken(token) {
token.invalidate()
this.onTokenInvalidated(token.id)
}
tokenForUrl(url) {
if (url.startsWith('/search')) {
return this.searchTokens.next()
} else {
return this.standardTokens.next()
}
}
// Act like request(), but tweak headers and query to avoid hitting a rate
// limit. Inject `request` so we can pass in `cachingRequest` from
// `request-handler.js`.
request(request, url, query, callback) {
const { baseUrl } = this
let token
let tokenString
if (this.withPooling) {
try {
token = this.tokenForUrl(url)
} catch (e) {
callback(e)
return
}
tokenString = token.id
} else {
tokenString = this.globalToken
}
const options = {
url,
baseUrl,
qs: query,
headers: {
'User-Agent': 'Shields.io',
Accept: 'application/vnd.github.v3+json',
Authorization: `token ${tokenString}`,
},
}
request(options, (err, res, buffer) => {
if (err === null) {
if (this.withPooling) {
if (res.statusCode === 401) {
this.invalidateToken(token)
} else if (res.statusCode < 500) {
this.updateToken(token, res.headers)
}
}
}
callback(err, res, buffer)
})
}
requestAsPromise(request, url, query) {
return new Promise((resolve, reject) => {
this.request(request, url, query, (err, res, buffer) => {
if (err) {
reject(err)
} else {
resolve({ res, buffer })
}
})
})
}
}
module.exports = GithubApiProvider