action-gh-release/node_modules/@octokit/plugin-throttling/dist-web/index.js
2021-07-21 15:17:14 +08:00

222 lines
8.2 KiB
JavaScript

import BottleneckLight from 'bottleneck/light';
const VERSION = "3.2.1";
const noop = () => Promise.resolve();
// @ts-ignore
function wrapRequest(state, request, options) {
return state.retryLimiter.schedule(doRequest, state, request, options);
}
// @ts-ignore
async function doRequest(state, request, options) {
const isWrite = options.method !== "GET" && options.method !== "HEAD";
const isSearch = options.method === "GET" && options.url.startsWith("/search/");
const isGraphQL = options.url.startsWith("/graphql");
const retryCount = ~~options.request.retryCount;
const jobOptions = retryCount > 0 ? { priority: 0, weight: 0 } : {};
if (state.clustering) {
// Remove a job from Redis if it has not completed or failed within 60s
// Examples: Node process terminated, client disconnected, etc.
// @ts-ignore
jobOptions.expiration = 1000 * 60;
}
// Guarantee at least 1000ms between writes
// GraphQL can also trigger writes
if (isWrite || isGraphQL) {
await state.write.key(state.id).schedule(jobOptions, noop);
}
// Guarantee at least 3000ms between requests that trigger notifications
if (isWrite && state.triggersNotification(options.url)) {
await state.notifications.key(state.id).schedule(jobOptions, noop);
}
// Guarantee at least 2000ms between search requests
if (isSearch) {
await state.search.key(state.id).schedule(jobOptions, noop);
}
const req = state.global.key(state.id).schedule(jobOptions, request, options);
if (isGraphQL) {
const res = await req;
if (res.data.errors != null &&
// @ts-ignore
res.data.errors.some((error) => error.type === "RATE_LIMITED")) {
const error = Object.assign(new Error("GraphQL Rate Limit Exceeded"), {
headers: res.headers,
data: res.data,
});
throw error;
}
}
return req;
}
var 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",
];
// @ts-ignore
function routeMatcher(paths) {
// EXAMPLE. For the following paths:
/* [
"/orgs/:org/invitations",
"/repos/:owner/:repo/collaborators/:username"
] */
// @ts-ignore
const regexes = paths.map((path) => path
.split("/")
// @ts-ignore
.map((c) => (c.startsWith(":") ? "(?:.+?)" : c))
.join("/"));
// 'regexes' would contain:
/* [
'/orgs/(?:.+?)/invitations',
'/repos/(?:.+?)/(?:.+?)/collaborators/(?:.+?)'
] */
// @ts-ignore
const regex = `^(?:${regexes.map((r) => `(?:${r})`).join("|")})[^/]*$`;
// 'regex' would contain:
/*
^(?:(?:\/orgs\/(?:.+?)\/invitations)|(?:\/repos\/(?:.+?)\/(?:.+?)\/collaborators\/(?:.+?)))[^\/]*$
It may look scary, but paste it into https://www.debuggex.com/
and it will make a lot more sense!
*/
return new RegExp(regex, "i");
}
// @ts-ignore
// Workaround to allow tests to directly access the triggersNotification function.
const regex = routeMatcher(triggersNotificationPaths);
const triggersNotification = regex.test.bind(regex);
const groups = {};
// @ts-ignore
const createGroups = function (Bottleneck, common) {
// @ts-ignore
groups.global = new Bottleneck.Group({
id: "octokit-global",
maxConcurrent: 10,
...common,
});
// @ts-ignore
groups.search = new Bottleneck.Group({
id: "octokit-search",
maxConcurrent: 1,
minTime: 2000,
...common,
});
// @ts-ignore
groups.write = new Bottleneck.Group({
id: "octokit-write",
maxConcurrent: 1,
minTime: 1000,
...common,
});
// @ts-ignore
groups.notifications = new Bottleneck.Group({
id: "octokit-notifications",
maxConcurrent: 1,
minTime: 3000,
...common,
});
};
function throttling(octokit, octokitOptions = {}) {
const { enabled = true, Bottleneck = BottleneckLight, id = "no-id", timeout = 1000 * 60 * 2, // Redis TTL: 2 minutes
connection, } = octokitOptions.throttle || {};
if (!enabled) {
return;
}
const common = { connection, timeout };
// @ts-ignore
if (groups.global == null) {
createGroups(Bottleneck, common);
}
const state = Object.assign({
clustering: connection != null,
triggersNotification,
minimumAbuseRetryAfter: 5,
retryAfterBaseValue: 1000,
retryLimiter: new Bottleneck(),
id,
...groups,
},
// @ts-ignore
octokitOptions.throttle);
if (typeof state.onAbuseLimit !== "function" ||
typeof state.onRateLimit !== "function") {
throw new Error(`octokit/plugin-throttling error:
You must pass the onAbuseLimit and onRateLimit error handlers.
See https://github.com/octokit/rest.js#throttling
const octokit = new Octokit({
throttle: {
onAbuseLimit: (retryAfter, options) => {/* ... */},
onRateLimit: (retryAfter, options) => {/* ... */}
}
})
`);
}
const events = {};
const emitter = new Bottleneck.Events(events);
// @ts-ignore
events.on("abuse-limit", state.onAbuseLimit);
// @ts-ignore
events.on("rate-limit", state.onRateLimit);
// @ts-ignore
events.on("error", (e) => console.warn("Error in throttling-plugin limit handler", e));
// @ts-ignore
state.retryLimiter.on("failed", async function (error, info) {
const options = info.args[info.args.length - 1];
const isGraphQL = options.url.startsWith("/graphql");
if (!(isGraphQL || error.status === 403)) {
return;
}
const retryCount = ~~options.request.retryCount;
options.request.retryCount = retryCount;
const { wantRetry, retryAfter } = await (async function () {
if (/\babuse\b/i.test(error.message)) {
// The user has hit the abuse rate limit. (REST only)
// https://developer.github.com/v3/#abuse-rate-limits
// The Retry-After header can sometimes be blank when hitting an abuse limit,
// but is always present after 2-3s, so make sure to set `retryAfter` to at least 5s by default.
const retryAfter = Math.max(~~error.headers["retry-after"], state.minimumAbuseRetryAfter);
const wantRetry = await emitter.trigger("abuse-limit", retryAfter, options);
return { wantRetry, retryAfter };
}
if (error.headers != null &&
error.headers["x-ratelimit-remaining"] === "0") {
// The user has used all their allowed calls for the current time period (REST and GraphQL)
// https://developer.github.com/v3/#rate-limiting
const rateLimitReset = new Date(~~error.headers["x-ratelimit-reset"] * 1000).getTime();
const retryAfter = Math.max(Math.ceil((rateLimitReset - Date.now()) / 1000), 0);
const wantRetry = await emitter.trigger("rate-limit", retryAfter, options);
return { wantRetry, retryAfter };
}
return {};
})();
if (wantRetry) {
options.request.retryCount++;
// @ts-ignore
return retryAfter * state.retryAfterBaseValue;
}
});
octokit.hook.wrap("request", wrapRequest.bind(null, state));
}
throttling.VERSION = VERSION;
throttling.triggersNotification = triggersNotification;
export { throttling };
//# sourceMappingURL=index.js.map