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, } = octokitOptions.throttle || {};
44 if (!enabled) {
45 return;
46 }
47 const common = { connection, timeout };
48 // @ts-ignore
49 if (groups.global == null) {
50 createGroups(Bottleneck, common);
51 }
52 const state = Object.assign({
53 clustering: connection != null,
54 triggersNotification,
55 minimumAbuseRetryAfter: 5,
56 retryAfterBaseValue: 1000,
57 retryLimiter: new Bottleneck(),
58 id,
59 ...groups,
60 },
61 // @ts-ignore
62 octokitOptions.throttle);
63 if (typeof state.onAbuseLimit !== "function" ||
64 typeof state.onRateLimit !== "function") {
65 throw new Error(`octokit/plugin-throttling error:
66 You must pass the onAbuseLimit and onRateLimit error handlers.
67 See https://github.com/octokit/rest.js#throttling
68
69 const octokit = new Octokit({
70 throttle: {
71 onAbuseLimit: (retryAfter, options) => {/* ... */},
72 onRateLimit: (retryAfter, options) => {/* ... */}
73 }
74 })
75 `);
76 }
77 const events = {};
78 const emitter = new Bottleneck.Events(events);
79 // @ts-ignore
80 events.on("abuse-limit", state.onAbuseLimit);
81 // @ts-ignore
82 events.on("rate-limit", state.onRateLimit);
83 // @ts-ignore
84 events.on("error", (e) => console.warn("Error in throttling-plugin limit handler", e));
85 // @ts-ignore
86 state.retryLimiter.on("failed", async function (error, info) {
87 const options = info.args[info.args.length - 1];
88 const isGraphQL = options.url.startsWith("/graphql");
89 if (!(isGraphQL || error.status === 403)) {
90 return;
91 }
92 const retryCount = ~~options.request.retryCount;
93 options.request.retryCount = retryCount;
94 const { wantRetry, retryAfter } = await (async function () {
95 if (/\babuse\b/i.test(error.message)) {
96 // The user has hit the abuse rate limit. (REST only)
97 // https://developer.github.com/v3/#abuse-rate-limits
98 // The Retry-After header can sometimes be blank when hitting an abuse limit,
99 // but is always present after 2-3s, so make sure to set `retryAfter` to at least 5s by default.
100 const retryAfter = Math.max(~~error.headers["retry-after"], state.minimumAbuseRetryAfter);
101 const wantRetry = await emitter.trigger("abuse-limit", retryAfter, options, octokit);
102 return { wantRetry, retryAfter };
103 }
104 if (error.headers != null &&
105 error.headers["x-ratelimit-remaining"] === "0") {
106 // The user has used all their allowed calls for the current time period (REST and GraphQL)
107 // https://developer.github.com/v3/#rate-limiting
108 const rateLimitReset = new Date(~~error.headers["x-ratelimit-reset"] * 1000).getTime();
109 const retryAfter = Math.max(Math.ceil((rateLimitReset - Date.now()) / 1000), 0);
110 const wantRetry = await emitter.trigger("rate-limit", retryAfter, options, octokit);
111 return { wantRetry, retryAfter };
112 }
113 return {};
114 })();
115 if (wantRetry) {
116 options.request.retryCount++;
117 // @ts-ignore
118 return retryAfter * state.retryAfterBaseValue;
119 }
120 });
121 octokit.hook.wrap("request", wrapRequest.bind(null, state));
122}
123throttling.VERSION = VERSION;
124throttling.triggersNotification = triggersNotification;