162 lines
4.0 KiB
JavaScript
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
|