Skip to content

Commit 64c537e

Browse files
committed
feat: TypeScript edition
BREAKING CHANGE: Instead of `const throttling = require("@octokit/throttling-plugin")`, do `const { throttling } = require("@octokit/throttling-plugin")`
1 parent f31bcd6 commit 64c537e

File tree

7 files changed

+247
-189
lines changed

7 files changed

+247
-189
lines changed

lib/index.js

Lines changed: 0 additions & 128 deletions
This file was deleted.

lib/wrap-request.js

Lines changed: 0 additions & 49 deletions
This file was deleted.

lib/triggers-notification-paths.json renamed to src/generated/triggers-notification-paths.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[
1+
const PATHS = [
22
"/orgs/:org/invitations",
33
"/repos/:owner/:repo/collaborators/:username",
44
"/repos/:owner/:repo/commits/:commit_sha/comments",
@@ -13,4 +13,5 @@
1313
"/repos/:owner/:repo/releases",
1414
"/teams/:team_id/discussions",
1515
"/teams/:team_id/discussions/:discussion_number/comments"
16-
]
16+
];
17+
export default PATHS;

src/index.ts

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

0 commit comments

Comments
 (0)