UNPKG

4.9 kBJavaScriptView Raw
1// @ts-ignore
2import BottleneckLight from "bottleneck/light";
3import { VERSION } from "./version";
4import { wrapRequest } from "./wrap-request";
5import triggersNotificationPaths from "./generated/triggers-notification-paths";
6import { routeMatcher } from "./route-matcher";
7// Workaround to allow tests to directly access the triggersNotification function.
8const regex = routeMatcher(triggersNotificationPaths);
9const triggersNotification = regex.test.bind(regex);
10const groups = {};
11// @ts-ignore
12const createGroups = function (Bottleneck, common) {
13 // @ts-ignore
14 groups.global = new Bottleneck.Group({
15 id: "octokit-global",
16 maxConcurrent: 10,
17 ...common
18 });
19 // @ts-ignore
20 groups.search = new Bottleneck.Group({
21 id: "octokit-search",
22 maxConcurrent: 1,
23 minTime: 2000,
24 ...common
25 });
26 // @ts-ignore
27 groups.write = new Bottleneck.Group({
28 id: "octokit-write",
29 maxConcurrent: 1,
30 minTime: 1000,
31 ...common
32 });
33 // @ts-ignore
34 groups.notifications = new Bottleneck.Group({
35 id: "octokit-notifications",
36 maxConcurrent: 1,
37 minTime: 3000,
38 ...common
39 });
40};
41export function throttling(octokit, octokitOptions = {}) {
42 const { enabled = true, Bottleneck = BottleneckLight, id = "no-id", timeout = 1000 * 60 * 2, // Redis TTL: 2 minutes
43 connection
44 // @ts-ignore
45 } = octokitOptions.throttle || {};
46 if (!enabled) {
47 return;
48 }
49 const common = { connection, timeout };
50 // @ts-ignore
51 if (groups.global == null) {
52 createGroups(Bottleneck, common);
53 }
54 const state = Object.assign({
55 clustering: connection != null,
56 triggersNotification,
57 minimumAbuseRetryAfter: 5,
58 retryAfterBaseValue: 1000,
59 retryLimiter: new Bottleneck(),
60 id,
61 ...groups
62 },
63 // @ts-ignore
64 octokitOptions.throttle);
65 if (typeof state.onAbuseLimit !== "function" ||
66 typeof state.onRateLimit !== "function") {
67 throw new Error(`octokit/plugin-throttling error:
68 You must pass the onAbuseLimit and onRateLimit error handlers.
69 See https://github.com/octokit/rest.js#throttling
70
71 const octokit = new Octokit({
72 throttle: {
73 onAbuseLimit: (retryAfter, options) => {/* ... */},
74 onRateLimit: (retryAfter, options) => {/* ... */}
75 }
76 })
77 `);
78 }
79 const events = {};
80 const emitter = new Bottleneck.Events(events);
81 // @ts-ignore
82 events.on("abuse-limit", state.onAbuseLimit);
83 // @ts-ignore
84 events.on("rate-limit", state.onRateLimit);
85 // @ts-ignore
86 events.on("error", e => console.warn("Error in throttling-plugin limit handler", e));
87 // @ts-ignore
88 state.retryLimiter.on("failed", async function (error, info) {
89 const options = info.args[info.args.length - 1];
90 const isGraphQL = options.url.startsWith("/graphql");
91 if (!(isGraphQL || error.status === 403)) {
92 return;
93 }
94 const retryCount = ~~options.request.retryCount;
95 options.request.retryCount = retryCount;
96 const { wantRetry, retryAfter } = await (async function () {
97 if (/\babuse\b/i.test(error.message)) {
98 // The user has hit the abuse rate limit. (REST only)
99 // https://developer.github.com/v3/#abuse-rate-limits
100 // The Retry-After header can sometimes be blank when hitting an abuse limit,
101 // but is always present after 2-3s, so make sure to set `retryAfter` to at least 5s by default.
102 const retryAfter = Math.max(~~error.headers["retry-after"], state.minimumAbuseRetryAfter);
103 const wantRetry = await emitter.trigger("abuse-limit", retryAfter, options);
104 return { wantRetry, retryAfter };
105 }
106 if (error.headers != null &&
107 error.headers["x-ratelimit-remaining"] === "0") {
108 // The user has used all their allowed calls for the current time period (REST and GraphQL)
109 // https://developer.github.com/v3/#rate-limiting
110 const rateLimitReset = new Date(~~error.headers["x-ratelimit-reset"] * 1000).getTime();
111 const retryAfter = Math.max(Math.ceil((rateLimitReset - Date.now()) / 1000), 0);
112 const wantRetry = await emitter.trigger("rate-limit", retryAfter, options);
113 return { wantRetry, retryAfter };
114 }
115 return {};
116 })();
117 if (wantRetry) {
118 options.request.retryCount++;
119 // @ts-ignore
120 return retryAfter * state.retryAfterBaseValue;
121 }
122 });
123 octokit.hook.wrap("request", wrapRequest.bind(null, state));
124}
125throttling.VERSION = VERSION;
126throttling.triggersNotification = triggersNotification;