UNPKG

9.52 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
6
7var BottleneckLight = _interopDefault(require('bottleneck/light'));
8
9function _defineProperty(obj, key, value) {
10 if (key in obj) {
11 Object.defineProperty(obj, key, {
12 value: value,
13 enumerable: true,
14 configurable: true,
15 writable: true
16 });
17 } else {
18 obj[key] = value;
19 }
20
21 return obj;
22}
23
24function ownKeys(object, enumerableOnly) {
25 var keys = Object.keys(object);
26
27 if (Object.getOwnPropertySymbols) {
28 var symbols = Object.getOwnPropertySymbols(object);
29 if (enumerableOnly) symbols = symbols.filter(function (sym) {
30 return Object.getOwnPropertyDescriptor(object, sym).enumerable;
31 });
32 keys.push.apply(keys, symbols);
33 }
34
35 return keys;
36}
37
38function _objectSpread2(target) {
39 for (var i = 1; i < arguments.length; i++) {
40 var source = arguments[i] != null ? arguments[i] : {};
41
42 if (i % 2) {
43 ownKeys(Object(source), true).forEach(function (key) {
44 _defineProperty(target, key, source[key]);
45 });
46 } else if (Object.getOwnPropertyDescriptors) {
47 Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
48 } else {
49 ownKeys(Object(source)).forEach(function (key) {
50 Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
51 });
52 }
53 }
54
55 return target;
56}
57
58const VERSION = "3.3.4";
59
60const noop = () => Promise.resolve(); // @ts-ignore
61
62
63function wrapRequest(state, request, options) {
64 return state.retryLimiter.schedule(doRequest, state, request, options);
65} // @ts-ignore
66
67async function doRequest(state, request, options) {
68 const isWrite = options.method !== "GET" && options.method !== "HEAD";
69 const isSearch = options.method === "GET" && options.url.startsWith("/search/");
70 const isGraphQL = options.url.startsWith("/graphql");
71 const retryCount = ~~options.request.retryCount;
72 const jobOptions = retryCount > 0 ? {
73 priority: 0,
74 weight: 0
75 } : {};
76
77 if (state.clustering) {
78 // Remove a job from Redis if it has not completed or failed within 60s
79 // Examples: Node process terminated, client disconnected, etc.
80 // @ts-ignore
81 jobOptions.expiration = 1000 * 60;
82 } // Guarantee at least 1000ms between writes
83 // GraphQL can also trigger writes
84
85
86 if (isWrite || isGraphQL) {
87 await state.write.key(state.id).schedule(jobOptions, noop);
88 } // Guarantee at least 3000ms between requests that trigger notifications
89
90
91 if (isWrite && state.triggersNotification(options.url)) {
92 await state.notifications.key(state.id).schedule(jobOptions, noop);
93 } // Guarantee at least 2000ms between search requests
94
95
96 if (isSearch) {
97 await state.search.key(state.id).schedule(jobOptions, noop);
98 }
99
100 const req = state.global.key(state.id).schedule(jobOptions, request, options);
101
102 if (isGraphQL) {
103 const res = await req;
104
105 if (res.data.errors != null && // @ts-ignore
106 res.data.errors.some(error => error.type === "RATE_LIMITED")) {
107 const error = Object.assign(new Error("GraphQL Rate Limit Exceeded"), {
108 headers: res.headers,
109 data: res.data
110 });
111 throw error;
112 }
113 }
114
115 return req;
116}
117
118var triggersNotificationPaths = ["/orgs/{org}/invitations", "/orgs/{org}/teams/{team_slug}/discussions", "/orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/comments", "/repos/{owner}/{repo}/collaborators/{username}", "/repos/{owner}/{repo}/commits/{commit_sha}/comments", "/repos/{owner}/{repo}/issues", "/repos/{owner}/{repo}/issues/{issue_number}/comments", "/repos/{owner}/{repo}/pulls", "/repos/{owner}/{repo}/pulls/{pull_number}/comments", "/repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies", "/repos/{owner}/{repo}/pulls/{pull_number}/merge", "/repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers", "/repos/{owner}/{repo}/pulls/{pull_number}/reviews", "/repos/{owner}/{repo}/releases", "/teams/{team_id}/discussions", "/teams/{team_id}/discussions/{discussion_number}/comments"];
119
120// @ts-ignore
121function routeMatcher(paths) {
122 // EXAMPLE. For the following paths:
123
124 /* [
125 "/orgs/:org/invitations",
126 "/repos/:owner/:repo/collaborators/:username"
127 ] */
128 // @ts-ignore
129 const regexes = paths.map(path => path.split("/") // @ts-ignore
130 .map(c => c.startsWith("{") ? "(?:.+?)" : c).join("/")); // 'regexes' would contain:
131
132 /* [
133 '/orgs/(?:.+?)/invitations',
134 '/repos/(?:.+?)/(?:.+?)/collaborators/(?:.+?)'
135 ] */
136 // @ts-ignore
137
138 const regex = `^(?:${regexes.map(r => `(?:${r})`).join("|")})[^/]*$`; // 'regex' would contain:
139
140 /*
141 ^(?:(?:\/orgs\/(?:.+?)\/invitations)|(?:\/repos\/(?:.+?)\/(?:.+?)\/collaborators\/(?:.+?)))[^\/]*$
142 It may look scary, but paste it into https://www.debuggex.com/
143 and it will make a lot more sense!
144 */
145
146 return new RegExp(regex, "i");
147}
148
149const regex = routeMatcher(triggersNotificationPaths);
150const triggersNotification = regex.test.bind(regex);
151const groups = {}; // @ts-ignore
152
153const createGroups = function (Bottleneck, common) {
154 // @ts-ignore
155 groups.global = new Bottleneck.Group(_objectSpread2({
156 id: "octokit-global",
157 maxConcurrent: 10
158 }, common)); // @ts-ignore
159
160 groups.search = new Bottleneck.Group(_objectSpread2({
161 id: "octokit-search",
162 maxConcurrent: 1,
163 minTime: 2000
164 }, common)); // @ts-ignore
165
166 groups.write = new Bottleneck.Group(_objectSpread2({
167 id: "octokit-write",
168 maxConcurrent: 1,
169 minTime: 1000
170 }, common)); // @ts-ignore
171
172 groups.notifications = new Bottleneck.Group(_objectSpread2({
173 id: "octokit-notifications",
174 maxConcurrent: 1,
175 minTime: 3000
176 }, common));
177};
178
179function throttling(octokit, octokitOptions = {}) {
180 const {
181 enabled = true,
182 Bottleneck = BottleneckLight,
183 id = "no-id",
184 timeout = 1000 * 60 * 2,
185 // Redis TTL: 2 minutes
186 connection
187 } = octokitOptions.throttle || {};
188
189 if (!enabled) {
190 return;
191 }
192
193 const common = {
194 connection,
195 timeout
196 }; // @ts-ignore
197
198 if (groups.global == null) {
199 createGroups(Bottleneck, common);
200 }
201
202 const state = Object.assign(_objectSpread2({
203 clustering: connection != null,
204 triggersNotification,
205 minimumAbuseRetryAfter: 5,
206 retryAfterBaseValue: 1000,
207 retryLimiter: new Bottleneck(),
208 id
209 }, groups), // @ts-ignore
210 octokitOptions.throttle);
211
212 if (typeof state.onAbuseLimit !== "function" || typeof state.onRateLimit !== "function") {
213 throw new Error(`octokit/plugin-throttling error:
214 You must pass the onAbuseLimit and onRateLimit error handlers.
215 See https://github.com/octokit/rest.js#throttling
216
217 const octokit = new Octokit({
218 throttle: {
219 onAbuseLimit: (retryAfter, options) => {/* ... */},
220 onRateLimit: (retryAfter, options) => {/* ... */}
221 }
222 })
223 `);
224 }
225
226 const events = {};
227 const emitter = new Bottleneck.Events(events); // @ts-ignore
228
229 events.on("abuse-limit", state.onAbuseLimit); // @ts-ignore
230
231 events.on("rate-limit", state.onRateLimit); // @ts-ignore
232
233 events.on("error", e => console.warn("Error in throttling-plugin limit handler", e)); // @ts-ignore
234
235 state.retryLimiter.on("failed", async function (error, info) {
236 const options = info.args[info.args.length - 1];
237 const shouldRetryGraphQL = options.url.startsWith("/graphql") && error.status !== 401;
238
239 if (!(shouldRetryGraphQL || error.status === 403)) {
240 return;
241 }
242
243 const retryCount = ~~options.request.retryCount;
244 options.request.retryCount = retryCount;
245 const {
246 wantRetry,
247 retryAfter
248 } = await async function () {
249 if (/\babuse\b/i.test(error.message)) {
250 // The user has hit the abuse rate limit. (REST and GraphQL)
251 // https://docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits
252 // The Retry-After header can sometimes be blank when hitting an abuse limit,
253 // but is always present after 2-3s, so make sure to set `retryAfter` to at least 5s by default.
254 const retryAfter = Math.max(~~error.headers["retry-after"], state.minimumAbuseRetryAfter);
255 const wantRetry = await emitter.trigger("abuse-limit", retryAfter, options, octokit);
256 return {
257 wantRetry,
258 retryAfter
259 };
260 }
261
262 if (error.headers != null && error.headers["x-ratelimit-remaining"] === "0") {
263 // The user has used all their allowed calls for the current time period (REST and GraphQL)
264 // https://docs.github.com/en/rest/reference/rate-limit (REST)
265 // https://docs.github.com/en/graphql/overview/resource-limitations#rate-limit (GraphQL)
266 const rateLimitReset = new Date(~~error.headers["x-ratelimit-reset"] * 1000).getTime();
267 const retryAfter = Math.max(Math.ceil((rateLimitReset - Date.now()) / 1000), 0);
268 const wantRetry = await emitter.trigger("rate-limit", retryAfter, options, octokit);
269 return {
270 wantRetry,
271 retryAfter
272 };
273 }
274
275 return {};
276 }();
277
278 if (wantRetry) {
279 options.request.retryCount++; // @ts-ignore
280
281 return retryAfter * state.retryAfterBaseValue;
282 }
283 });
284 octokit.hook.wrap("request", wrapRequest.bind(null, state));
285}
286throttling.VERSION = VERSION;
287throttling.triggersNotification = triggersNotification;
288
289exports.throttling = throttling;
290//# sourceMappingURL=index.js.map