Skip to content

Commit 034a34d

Browse files
Simon Grondingr2m
authored andcommitted
feat: initial version
1 parent a102985 commit 034a34d

File tree

2 files changed

+116
-0
lines changed

2 files changed

+116
-0
lines changed

lib/index.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
module.exports = throttlingPlugin
2+
3+
const Bottleneck = require('bottleneck/light')
4+
const wrapRequest = require('./wrap-request')
5+
6+
const triggersNotificationRoutes = [
7+
'/repos/:owner/:repo/issues',
8+
'/repos/:owner/:repo/issues/:number/comments',
9+
'/orgs/:org/invitations',
10+
'/repos/:owner/:repo/pulls',
11+
'/repos/:owner/:repo/pulls',
12+
'/repos/:owner/:repo/pulls/:number/merge',
13+
'/repos/:owner/:repo/pulls/:number/reviews',
14+
'/repos/:owner/:repo/pulls/:number/comments',
15+
'/repos/:owner/:repo/pulls/:number/comments',
16+
'/repos/:owner/:repo/pulls/:number/requested_reviewers',
17+
'/repos/:owner/:repo/collaborators/:username',
18+
'/repos/:owner/:repo/commits/:sha/comments',
19+
'/repos/:owner/:repo/releases',
20+
'/teams/:team_id/discussions',
21+
'/teams/:team_id/discussions/:discussion_number/comments'
22+
]
23+
24+
function buildLookup (arr) {
25+
return arr.reduce(function (acc, elem) {
26+
acc[elem] = true
27+
return acc
28+
}, {})
29+
}
30+
31+
function throttlingPlugin (octokit) {
32+
const state = {
33+
triggersNotification: buildLookup(triggersNotificationRoutes),
34+
minimumAbuseRetryAfter: 5,
35+
maxRetries: 1,
36+
globalLimiter: new Bottleneck({
37+
maxConcurrent: 1
38+
}),
39+
writeLimiter: new Bottleneck({
40+
maxConcurrent: 1,
41+
minTime: 1000
42+
}),
43+
triggersNotificationLimiter: new Bottleneck({
44+
maxConcurrent: 1,
45+
minTime: 3000
46+
})
47+
}
48+
49+
octokit.throttle = {
50+
options: (options = {}) => Object.assign(state, options)
51+
}
52+
const emitter = new Bottleneck.Events(octokit.throttle)
53+
54+
octokit.hook.wrap('request', wrapRequest.bind(null, state, emitter))
55+
}

lib/wrap-request.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/* eslint padded-blocks: 0 */
2+
module.exports = wrapRequest
3+
4+
const noop = () => Promise.resolve()
5+
6+
async function wrapRequest (state, emitter, request, options) {
7+
const retryRequest = function (after) {
8+
return new Promise(resolve => setTimeout(resolve, after * 1000))
9+
.then(() => wrapRequest(state, emitter, request, options))
10+
}
11+
const isWrite = options.method !== 'GET' && options.method !== 'HEAD'
12+
const retryCount = ~~options.request.retryCount
13+
const jobOptions = retryCount > 0 ? { priority: 0, weight: 0 } : {}
14+
15+
// Guarantee at least 1000ms between writes
16+
if (isWrite) {
17+
await state.writeLimiter.schedule(noop)
18+
}
19+
20+
// Guarantee at least 3000ms between requests that trigger notifications
21+
if (isWrite && state.triggersNotification[options.url]) {
22+
await state.triggersNotificationLimiter.schedule(noop)
23+
}
24+
25+
return state.globalLimiter.schedule(jobOptions, async function () {
26+
try {
27+
// Execute request
28+
return await request(options)
29+
} catch (error) {
30+
if (error.status === 403 && /\babuse\b/i.test(error.message)) {
31+
// The user has hit the abuse rate limit.
32+
// https://developer.github.com/v3/#abuse-rate-limits
33+
34+
// The Retry-After header can sometimes be blank when hitting an abuse limit,
35+
// but is always present after 2-3s, so make sure to set `retryAfter` to at least 5s by default.
36+
const retryAfter = Math.max(~~error.headers['retry-after'], state.minimumAbuseRetryAfter)
37+
emitter.trigger('abuse-limit', retryAfter)
38+
39+
if (state.maxRetries > retryCount) {
40+
options.request.retryCount = retryCount + 1
41+
return retryRequest(retryAfter)
42+
}
43+
44+
} else if (error.status === 403 && error.headers['x-ratelimit-remaining'] === '0') {
45+
// The user has used all their allowed calls for the current time period
46+
// https://developer.github.com/v3/#rate-limiting
47+
48+
const rateLimitReset = new Date(~~error.headers['x-ratelimit-reset'] * 1000).getTime()
49+
const retryAfter = Math.max(Math.ceil((rateLimitReset - Date.now()) / 1000), 0)
50+
emitter.trigger('rate-limit', retryAfter)
51+
52+
if (state.maxRetries > retryCount) {
53+
options.request.retryCount = retryCount + 1
54+
return retryRequest(retryAfter)
55+
}
56+
}
57+
58+
throw error
59+
}
60+
})
61+
}

0 commit comments

Comments
 (0)