Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@octokit/plugin-throttling

Package Overview
Dependencies
Maintainers
4
Versions
70
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@octokit/plugin-throttling - npm Package Compare versions

Comparing version 6.0.0 to 6.0.1

242

dist-node/index.js

@@ -1,45 +0,62 @@

'use strict';
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
Object.defineProperty(exports, '__esModule', { value: true });
// pkg/dist-src/index.js
var dist_src_exports = {};
__export(dist_src_exports, {
throttling: () => throttling
});
module.exports = __toCommonJS(dist_src_exports);
var import_light = __toESM(require("bottleneck/light"));
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
// pkg/dist-src/version.js
var VERSION = "6.0.1";
var BottleneckLight = _interopDefault(require('bottleneck/light'));
const VERSION = "6.0.0";
const noop = () => Promise.resolve();
// @ts-expect-error
// pkg/dist-src/wrap-request.js
var noop = () => Promise.resolve();
function wrapRequest(state, request, options) {
return state.retryLimiter.schedule(doRequest, state, request, options);
}
// @ts-expect-error
async function doRequest(state, request, options) {
const isWrite = options.method !== "GET" && options.method !== "HEAD";
const {
pathname
} = new URL(options.url, "http://github.test");
const { pathname } = new URL(options.url, "http://github.test");
const isSearch = options.method === "GET" && pathname.startsWith("/search/");
const isGraphQL = pathname.startsWith("/graphql");
const retryCount = ~~request.retryCount;
const jobOptions = retryCount > 0 ? {
priority: 0,
weight: 0
} : {};
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-expect-error
jobOptions.expiration = 1000 * 60;
jobOptions.expiration = 1e3 * 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(pathname)) {
await state.notifications.key(state.id).schedule(jobOptions, noop);
}
// Guarantee at least 2000ms between search requests
if (isSearch) {

@@ -51,5 +68,4 @@ await state.search.key(state.id).schedule(jobOptions, noop);

const res = await req;
if (res.data.errors != null &&
// @ts-expect-error
res.data.errors.some(error => error.type === "RATE_LIMITED")) {
if (res.data.errors != null && // @ts-expect-error
res.data.errors.some((error) => error.type === "RATE_LIMITED")) {
const error = Object.assign(new Error("GraphQL Rate Limit Exceeded"), {

@@ -65,33 +81,37 @@ response: res,

var triggersNotificationPaths = ["/orgs/{org}/invitations", "/orgs/{org}/invitations/{invitation_id}", "/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"];
// pkg/dist-src/generated/triggers-notification-paths.js
var triggers_notification_paths_default = [
"/orgs/{org}/invitations",
"/orgs/{org}/invitations/{invitation_id}",
"/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"
];
// pkg/dist-src/route-matcher.js
function routeMatcher(paths) {
// EXAMPLE. For the following paths:
/* [
"/orgs/{org}/invitations",
"/repos/{owner}/{repo}/collaborators/{username}"
] */
const regexes = paths.map(path => path.split("/").map(c => c.startsWith("{") ? "(?:.+?)" : c).join("/"));
// 'regexes' would contain:
/* [
'/orgs/(?:.+?)/invitations',
'/repos/(?:.+?)/(?:.+?)/collaborators/(?:.+?)'
] */
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");
const regexes = paths.map(
(path) => path.split("/").map((c) => c.startsWith("{") ? "(?:.+?)" : c).join("/")
);
const regex2 = `^(?:${regexes.map((r) => `(?:${r})`).join("|")})[^/]*$`;
return new RegExp(regex2, "i");
}
// @ts-expect-error
// Workaround to allow tests to directly access the triggersNotification function.
const regex = routeMatcher(triggersNotificationPaths);
const triggersNotification = regex.test.bind(regex);
const groups = {};
// @ts-expect-error
const createGroups = function (Bottleneck, common) {
// pkg/dist-src/index.js
var regex = routeMatcher(triggers_notification_paths_default);
var triggersNotification = regex.test.bind(regex);
var groups = {};
var createGroups = function(Bottleneck, common) {
groups.global = new Bottleneck.Group({

@@ -105,3 +125,3 @@ id: "octokit-global",

maxConcurrent: 1,
minTime: 2000,
minTime: 2e3,
...common

@@ -112,3 +132,3 @@ });

maxConcurrent: 1,
minTime: 1000,
minTime: 1e3,
...common

@@ -119,3 +139,3 @@ });

maxConcurrent: 1,
minTime: 3000,
minTime: 3e3,
...common

@@ -127,5 +147,5 @@ });

enabled = true,
Bottleneck = BottleneckLight,
Bottleneck = import_light.default,
id = "no-id",
timeout = 1000 * 60 * 2,
timeout = 1e3 * 60 * 2,
// Redis TTL: 2 minutes

@@ -137,18 +157,18 @@ connection

}
const common = {
connection,
timeout
};
const common = { connection, timeout };
if (groups.global == null) {
createGroups(Bottleneck, common);
}
const state = Object.assign({
clustering: connection != null,
triggersNotification,
fallbackSecondaryRateRetryAfter: 60,
retryAfterBaseValue: 1000,
retryLimiter: new Bottleneck(),
id,
...groups
}, octokitOptions.throttle);
const state = Object.assign(
{
clustering: connection != null,
triggersNotification,
fallbackSecondaryRateRetryAfter: 60,
retryAfterBaseValue: 1e3,
retryLimiter: new Bottleneck(),
id,
...groups
},
octokitOptions.throttle
);
if (typeof state.onSecondaryRateLimit !== "function" || typeof state.onRateLimit !== "function") {

@@ -169,14 +189,11 @@ throw new Error(`octokit/plugin-throttling error:

const emitter = new Bottleneck.Events(events);
// @ts-expect-error
events.on("secondary-limit", state.onSecondaryRateLimit);
// @ts-expect-error
events.on("rate-limit", state.onRateLimit);
// @ts-expect-error
events.on("error", e => octokit.log.warn("Error in throttling-plugin limit handler", e));
// @ts-expect-error
state.retryLimiter.on("failed", async function (error, info) {
const [state, request, options] = info.args;
const {
pathname
} = new URL(options.url, "http://github.test");
events.on(
"error",
(e) => octokit.log.warn("Error in throttling-plugin limit handler", e)
);
state.retryLimiter.on("failed", async function(error, info) {
const [state2, request, options] = info.args;
const { pathname } = new URL(options.url, "http://github.test");
const shouldRetryGraphQL = pathname.startsWith("/graphql") && error.status !== 401;

@@ -188,31 +205,31 @@ if (!(shouldRetryGraphQL || error.status === 403)) {

request.retryCount = retryCount;
// backward compatibility
options.request.retryCount = retryCount;
const {
wantRetry,
retryAfter = 0
} = await async function () {
const { wantRetry, retryAfter = 0 } = await async function() {
if (/\bsecondary rate\b/i.test(error.message)) {
// The user has hit the secondary rate limit. (REST and GraphQL)
// https://docs.github.com/en/rest/overview/resources-in-the-rest-api#secondary-rate-limits
// The Retry-After header can sometimes be blank when hitting a secondary rate limit,
// but is always present after 2-3s, so make sure to set `retryAfter` to at least 5s by default.
const retryAfter = Number(error.response.headers["retry-after"]) || state.fallbackSecondaryRateRetryAfter;
const wantRetry = await emitter.trigger("secondary-limit", retryAfter, options, octokit, retryCount);
return {
wantRetry,
retryAfter
};
const retryAfter2 = Number(error.response.headers["retry-after"]) || state2.fallbackSecondaryRateRetryAfter;
const wantRetry2 = await emitter.trigger(
"secondary-limit",
retryAfter2,
options,
octokit,
retryCount
);
return { wantRetry: wantRetry2, retryAfter: retryAfter2 };
}
if (error.response.headers != null && error.response.headers["x-ratelimit-remaining"] === "0") {
// The user has used all their allowed calls for the current time period (REST and GraphQL)
// https://docs.github.com/en/rest/reference/rate-limit (REST)
// https://docs.github.com/en/graphql/overview/resource-limitations#rate-limit (GraphQL)
const rateLimitReset = new Date(~~error.response.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, octokit, retryCount);
return {
wantRetry,
retryAfter
};
const rateLimitReset = new Date(
~~error.response.headers["x-ratelimit-reset"] * 1e3
).getTime();
const retryAfter2 = Math.max(
Math.ceil((rateLimitReset - Date.now()) / 1e3),
0
);
const wantRetry2 = await emitter.trigger(
"rate-limit",
retryAfter2,
options,
octokit,
retryCount
);
return { wantRetry: wantRetry2, retryAfter: retryAfter2 };
}

@@ -223,3 +240,3 @@ return {};

request.retryCount++;
return retryAfter * state.retryAfterBaseValue;
return retryAfter * state2.retryAfterBaseValue;
}

@@ -232,4 +249,5 @@ });

throttling.triggersNotification = triggersNotification;
exports.throttling = throttling;
//# sourceMappingURL=index.js.map
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
throttling
});

@@ -1,19 +0,22 @@

export default [
"/orgs/{org}/invitations",
"/orgs/{org}/invitations/{invitation_id}",
"/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",
var triggers_notification_paths_default = [
"/orgs/{org}/invitations",
"/orgs/{org}/invitations/{invitation_id}",
"/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"
];
export {
triggers_notification_paths_default as default
};

@@ -1,2 +0,1 @@

// @ts-expect-error
import BottleneckLight from "bottleneck/light";

@@ -7,54 +6,60 @@ import { VERSION } from "./version";

import { routeMatcher } from "./route-matcher";
// Workaround to allow tests to directly access the triggersNotification function.
const regex = routeMatcher(triggersNotificationPaths);
const triggersNotification = regex.test.bind(regex);
const groups = {};
// @ts-expect-error
const createGroups = function (Bottleneck, common) {
groups.global = new Bottleneck.Group({
id: "octokit-global",
maxConcurrent: 10,
...common,
});
groups.search = new Bottleneck.Group({
id: "octokit-search",
maxConcurrent: 1,
minTime: 2000,
...common,
});
groups.write = new Bottleneck.Group({
id: "octokit-write",
maxConcurrent: 1,
minTime: 1000,
...common,
});
groups.notifications = new Bottleneck.Group({
id: "octokit-notifications",
maxConcurrent: 1,
minTime: 3000,
...common,
});
const createGroups = function(Bottleneck, common) {
groups.global = new Bottleneck.Group({
id: "octokit-global",
maxConcurrent: 10,
...common
});
groups.search = new Bottleneck.Group({
id: "octokit-search",
maxConcurrent: 1,
minTime: 2e3,
...common
});
groups.write = new Bottleneck.Group({
id: "octokit-write",
maxConcurrent: 1,
minTime: 1e3,
...common
});
groups.notifications = new Bottleneck.Group({
id: "octokit-notifications",
maxConcurrent: 1,
minTime: 3e3,
...common
});
};
export 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 };
if (groups.global == null) {
createGroups(Bottleneck, common);
}
const state = Object.assign({
clustering: connection != null,
triggersNotification,
fallbackSecondaryRateRetryAfter: 60,
retryAfterBaseValue: 1000,
retryLimiter: new Bottleneck(),
id,
...groups,
}, octokitOptions.throttle);
if (typeof state.onSecondaryRateLimit !== "function" ||
typeof state.onRateLimit !== "function") {
throw new Error(`octokit/plugin-throttling error:
function throttling(octokit, octokitOptions) {
const {
enabled = true,
Bottleneck = BottleneckLight,
id = "no-id",
timeout = 1e3 * 60 * 2,
// Redis TTL: 2 minutes
connection
} = octokitOptions.throttle || {};
if (!enabled) {
return {};
}
const common = { connection, timeout };
if (groups.global == null) {
createGroups(Bottleneck, common);
}
const state = Object.assign(
{
clustering: connection != null,
triggersNotification,
fallbackSecondaryRateRetryAfter: 60,
retryAfterBaseValue: 1e3,
retryLimiter: new Bottleneck(),
id,
...groups
},
octokitOptions.throttle
);
if (typeof state.onSecondaryRateLimit !== "function" || typeof state.onRateLimit !== "function") {
throw new Error(`octokit/plugin-throttling error:
You must pass the onSecondaryRateLimit and onRateLimit error handlers.

@@ -70,55 +75,64 @@ See https://octokit.github.io/rest.js/#throttling

`);
}
const events = {};
const emitter = new Bottleneck.Events(events);
events.on("secondary-limit", state.onSecondaryRateLimit);
events.on("rate-limit", state.onRateLimit);
events.on(
"error",
(e) => octokit.log.warn("Error in throttling-plugin limit handler", e)
);
state.retryLimiter.on("failed", async function(error, info) {
const [state2, request, options] = info.args;
const { pathname } = new URL(options.url, "http://github.test");
const shouldRetryGraphQL = pathname.startsWith("/graphql") && error.status !== 401;
if (!(shouldRetryGraphQL || error.status === 403)) {
return;
}
const events = {};
const emitter = new Bottleneck.Events(events);
// @ts-expect-error
events.on("secondary-limit", state.onSecondaryRateLimit);
// @ts-expect-error
events.on("rate-limit", state.onRateLimit);
// @ts-expect-error
events.on("error", (e) => octokit.log.warn("Error in throttling-plugin limit handler", e));
// @ts-expect-error
state.retryLimiter.on("failed", async function (error, info) {
const [state, request, options] = info.args;
const { pathname } = new URL(options.url, "http://github.test");
const shouldRetryGraphQL = pathname.startsWith("/graphql") && error.status !== 401;
if (!(shouldRetryGraphQL || error.status === 403)) {
return;
}
const retryCount = ~~request.retryCount;
request.retryCount = retryCount;
// backward compatibility
options.request.retryCount = retryCount;
const { wantRetry, retryAfter = 0 } = await (async function () {
if (/\bsecondary rate\b/i.test(error.message)) {
// The user has hit the secondary rate limit. (REST and GraphQL)
// https://docs.github.com/en/rest/overview/resources-in-the-rest-api#secondary-rate-limits
// The Retry-After header can sometimes be blank when hitting a secondary rate limit,
// but is always present after 2-3s, so make sure to set `retryAfter` to at least 5s by default.
const retryAfter = Number(error.response.headers["retry-after"]) ||
state.fallbackSecondaryRateRetryAfter;
const wantRetry = await emitter.trigger("secondary-limit", retryAfter, options, octokit, retryCount);
return { wantRetry, retryAfter };
}
if (error.response.headers != null &&
error.response.headers["x-ratelimit-remaining"] === "0") {
// The user has used all their allowed calls for the current time period (REST and GraphQL)
// https://docs.github.com/en/rest/reference/rate-limit (REST)
// https://docs.github.com/en/graphql/overview/resource-limitations#rate-limit (GraphQL)
const rateLimitReset = new Date(~~error.response.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, octokit, retryCount);
return { wantRetry, retryAfter };
}
return {};
})();
if (wantRetry) {
request.retryCount++;
return retryAfter * state.retryAfterBaseValue;
}
});
octokit.hook.wrap("request", wrapRequest.bind(null, state));
return {};
const retryCount = ~~request.retryCount;
request.retryCount = retryCount;
options.request.retryCount = retryCount;
const { wantRetry, retryAfter = 0 } = await async function() {
if (/\bsecondary rate\b/i.test(error.message)) {
const retryAfter2 = Number(error.response.headers["retry-after"]) || state2.fallbackSecondaryRateRetryAfter;
const wantRetry2 = await emitter.trigger(
"secondary-limit",
retryAfter2,
options,
octokit,
retryCount
);
return { wantRetry: wantRetry2, retryAfter: retryAfter2 };
}
if (error.response.headers != null && error.response.headers["x-ratelimit-remaining"] === "0") {
const rateLimitReset = new Date(
~~error.response.headers["x-ratelimit-reset"] * 1e3
).getTime();
const retryAfter2 = Math.max(
Math.ceil((rateLimitReset - Date.now()) / 1e3),
0
);
const wantRetry2 = await emitter.trigger(
"rate-limit",
retryAfter2,
options,
octokit,
retryCount
);
return { wantRetry: wantRetry2, retryAfter: retryAfter2 };
}
return {};
}();
if (wantRetry) {
request.retryCount++;
return retryAfter * state2.retryAfterBaseValue;
}
});
octokit.hook.wrap("request", wrapRequest.bind(null, state));
return {};
}
throttling.VERSION = VERSION;
throttling.triggersNotification = triggersNotification;
export {
throttling
};

@@ -1,25 +0,10 @@

export function routeMatcher(paths) {
// EXAMPLE. For the following paths:
/* [
"/orgs/{org}/invitations",
"/repos/{owner}/{repo}/collaborators/{username}"
] */
const regexes = paths.map((path) => path
.split("/")
.map((c) => (c.startsWith("{") ? "(?:.+?)" : c))
.join("/"));
// 'regexes' would contain:
/* [
'/orgs/(?:.+?)/invitations',
'/repos/(?:.+?)/(?:.+?)/collaborators/(?:.+?)'
] */
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");
function routeMatcher(paths) {
const regexes = paths.map(
(path) => path.split("/").map((c) => c.startsWith("{") ? "(?:.+?)" : c).join("/")
);
const regex = `^(?:${regexes.map((r) => `(?:${r})`).join("|")})[^/]*$`;
return new RegExp(regex, "i");
}
export {
routeMatcher
};

@@ -1,1 +0,4 @@

export const VERSION = "6.0.0";
const VERSION = "6.0.1";
export {
VERSION
};
const noop = () => Promise.resolve();
// @ts-expect-error
export function wrapRequest(state, request, options) {
return state.retryLimiter.schedule(doRequest, state, request, options);
function wrapRequest(state, request, options) {
return state.retryLimiter.schedule(doRequest, state, request, options);
}
// @ts-expect-error
async function doRequest(state, request, options) {
const isWrite = options.method !== "GET" && options.method !== "HEAD";
const { pathname } = new URL(options.url, "http://github.test");
const isSearch = options.method === "GET" && pathname.startsWith("/search/");
const isGraphQL = pathname.startsWith("/graphql");
const retryCount = ~~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-expect-error
jobOptions.expiration = 1000 * 60;
const isWrite = options.method !== "GET" && options.method !== "HEAD";
const { pathname } = new URL(options.url, "http://github.test");
const isSearch = options.method === "GET" && pathname.startsWith("/search/");
const isGraphQL = pathname.startsWith("/graphql");
const retryCount = ~~request.retryCount;
const jobOptions = retryCount > 0 ? { priority: 0, weight: 0 } : {};
if (state.clustering) {
jobOptions.expiration = 1e3 * 60;
}
if (isWrite || isGraphQL) {
await state.write.key(state.id).schedule(jobOptions, noop);
}
if (isWrite && state.triggersNotification(pathname)) {
await state.notifications.key(state.id).schedule(jobOptions, noop);
}
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-expect-error
res.data.errors.some((error) => error.type === "RATE_LIMITED")) {
const error = Object.assign(new Error("GraphQL Rate Limit Exceeded"), {
response: res,
data: res.data
});
throw error;
}
// 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(pathname)) {
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-expect-error
res.data.errors.some((error) => error.type === "RATE_LIMITED")) {
const error = Object.assign(new Error("GraphQL Rate Limit Exceeded"), {
response: res,
data: res.data,
});
throw error;
}
}
return req;
}
return req;
}
export {
wrapRequest
};
import { Octokit } from "@octokit/core";
import { OctokitOptions } from "@octokit/core/dist-types/types.d";
import { ThrottlingOptions } from "./types";
import type { OctokitOptions } from "@octokit/core/dist-types/types.d";
import type { ThrottlingOptions } from "./types";
export declare function throttling(octokit: Octokit, octokitOptions: OctokitOptions): {};

@@ -5,0 +5,0 @@ export declare namespace throttling {

@@ -1,1 +0,1 @@

export declare const VERSION = "6.0.0";
export declare const VERSION = "6.0.1";

@@ -1,152 +0,135 @@

import BottleneckLight from 'bottleneck/light';
// pkg/dist-src/index.js
import BottleneckLight from "bottleneck/light";
const VERSION = "6.0.0";
// pkg/dist-src/version.js
var VERSION = "6.0.1";
const noop = () => Promise.resolve();
// @ts-expect-error
// pkg/dist-src/wrap-request.js
var noop = () => Promise.resolve();
function wrapRequest(state, request, options) {
return state.retryLimiter.schedule(doRequest, state, request, options);
return state.retryLimiter.schedule(doRequest, state, request, options);
}
// @ts-expect-error
async function doRequest(state, request, options) {
const isWrite = options.method !== "GET" && options.method !== "HEAD";
const { pathname } = new URL(options.url, "http://github.test");
const isSearch = options.method === "GET" && pathname.startsWith("/search/");
const isGraphQL = pathname.startsWith("/graphql");
const retryCount = ~~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-expect-error
jobOptions.expiration = 1000 * 60;
const isWrite = options.method !== "GET" && options.method !== "HEAD";
const { pathname } = new URL(options.url, "http://github.test");
const isSearch = options.method === "GET" && pathname.startsWith("/search/");
const isGraphQL = pathname.startsWith("/graphql");
const retryCount = ~~request.retryCount;
const jobOptions = retryCount > 0 ? { priority: 0, weight: 0 } : {};
if (state.clustering) {
jobOptions.expiration = 1e3 * 60;
}
if (isWrite || isGraphQL) {
await state.write.key(state.id).schedule(jobOptions, noop);
}
if (isWrite && state.triggersNotification(pathname)) {
await state.notifications.key(state.id).schedule(jobOptions, noop);
}
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-expect-error
res.data.errors.some((error) => error.type === "RATE_LIMITED")) {
const error = Object.assign(new Error("GraphQL Rate Limit Exceeded"), {
response: res,
data: res.data
});
throw error;
}
// 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(pathname)) {
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-expect-error
res.data.errors.some((error) => error.type === "RATE_LIMITED")) {
const error = Object.assign(new Error("GraphQL Rate Limit Exceeded"), {
response: res,
data: res.data,
});
throw error;
}
}
return req;
}
return req;
}
var triggersNotificationPaths = [
"/orgs/{org}/invitations",
"/orgs/{org}/invitations/{invitation_id}",
"/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",
// pkg/dist-src/generated/triggers-notification-paths.js
var triggers_notification_paths_default = [
"/orgs/{org}/invitations",
"/orgs/{org}/invitations/{invitation_id}",
"/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"
];
// pkg/dist-src/route-matcher.js
function routeMatcher(paths) {
// EXAMPLE. For the following paths:
/* [
"/orgs/{org}/invitations",
"/repos/{owner}/{repo}/collaborators/{username}"
] */
const regexes = paths.map((path) => path
.split("/")
.map((c) => (c.startsWith("{") ? "(?:.+?)" : c))
.join("/"));
// 'regexes' would contain:
/* [
'/orgs/(?:.+?)/invitations',
'/repos/(?:.+?)/(?:.+?)/collaborators/(?:.+?)'
] */
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");
const regexes = paths.map(
(path) => path.split("/").map((c) => c.startsWith("{") ? "(?:.+?)" : c).join("/")
);
const regex2 = `^(?:${regexes.map((r) => `(?:${r})`).join("|")})[^/]*$`;
return new RegExp(regex2, "i");
}
// @ts-expect-error
// Workaround to allow tests to directly access the triggersNotification function.
const regex = routeMatcher(triggersNotificationPaths);
const triggersNotification = regex.test.bind(regex);
const groups = {};
// @ts-expect-error
const createGroups = function (Bottleneck, common) {
groups.global = new Bottleneck.Group({
id: "octokit-global",
maxConcurrent: 10,
...common,
});
groups.search = new Bottleneck.Group({
id: "octokit-search",
maxConcurrent: 1,
minTime: 2000,
...common,
});
groups.write = new Bottleneck.Group({
id: "octokit-write",
maxConcurrent: 1,
minTime: 1000,
...common,
});
groups.notifications = new Bottleneck.Group({
id: "octokit-notifications",
maxConcurrent: 1,
minTime: 3000,
...common,
});
// pkg/dist-src/index.js
var regex = routeMatcher(triggers_notification_paths_default);
var triggersNotification = regex.test.bind(regex);
var groups = {};
var createGroups = function(Bottleneck, common) {
groups.global = new Bottleneck.Group({
id: "octokit-global",
maxConcurrent: 10,
...common
});
groups.search = new Bottleneck.Group({
id: "octokit-search",
maxConcurrent: 1,
minTime: 2e3,
...common
});
groups.write = new Bottleneck.Group({
id: "octokit-write",
maxConcurrent: 1,
minTime: 1e3,
...common
});
groups.notifications = new Bottleneck.Group({
id: "octokit-notifications",
maxConcurrent: 1,
minTime: 3e3,
...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 };
if (groups.global == null) {
createGroups(Bottleneck, common);
}
const state = Object.assign({
clustering: connection != null,
triggersNotification,
fallbackSecondaryRateRetryAfter: 60,
retryAfterBaseValue: 1000,
retryLimiter: new Bottleneck(),
id,
...groups,
}, octokitOptions.throttle);
if (typeof state.onSecondaryRateLimit !== "function" ||
typeof state.onRateLimit !== "function") {
throw new Error(`octokit/plugin-throttling error:
const {
enabled = true,
Bottleneck = BottleneckLight,
id = "no-id",
timeout = 1e3 * 60 * 2,
// Redis TTL: 2 minutes
connection
} = octokitOptions.throttle || {};
if (!enabled) {
return {};
}
const common = { connection, timeout };
if (groups.global == null) {
createGroups(Bottleneck, common);
}
const state = Object.assign(
{
clustering: connection != null,
triggersNotification,
fallbackSecondaryRateRetryAfter: 60,
retryAfterBaseValue: 1e3,
retryLimiter: new Bottleneck(),
id,
...groups
},
octokitOptions.throttle
);
if (typeof state.onSecondaryRateLimit !== "function" || typeof state.onRateLimit !== "function") {
throw new Error(`octokit/plugin-throttling error:
You must pass the onSecondaryRateLimit and onRateLimit error handlers.

@@ -162,58 +145,64 @@ See https://octokit.github.io/rest.js/#throttling

`);
}
const events = {};
const emitter = new Bottleneck.Events(events);
events.on("secondary-limit", state.onSecondaryRateLimit);
events.on("rate-limit", state.onRateLimit);
events.on(
"error",
(e) => octokit.log.warn("Error in throttling-plugin limit handler", e)
);
state.retryLimiter.on("failed", async function(error, info) {
const [state2, request, options] = info.args;
const { pathname } = new URL(options.url, "http://github.test");
const shouldRetryGraphQL = pathname.startsWith("/graphql") && error.status !== 401;
if (!(shouldRetryGraphQL || error.status === 403)) {
return;
}
const events = {};
const emitter = new Bottleneck.Events(events);
// @ts-expect-error
events.on("secondary-limit", state.onSecondaryRateLimit);
// @ts-expect-error
events.on("rate-limit", state.onRateLimit);
// @ts-expect-error
events.on("error", (e) => octokit.log.warn("Error in throttling-plugin limit handler", e));
// @ts-expect-error
state.retryLimiter.on("failed", async function (error, info) {
const [state, request, options] = info.args;
const { pathname } = new URL(options.url, "http://github.test");
const shouldRetryGraphQL = pathname.startsWith("/graphql") && error.status !== 401;
if (!(shouldRetryGraphQL || error.status === 403)) {
return;
}
const retryCount = ~~request.retryCount;
request.retryCount = retryCount;
// backward compatibility
options.request.retryCount = retryCount;
const { wantRetry, retryAfter = 0 } = await (async function () {
if (/\bsecondary rate\b/i.test(error.message)) {
// The user has hit the secondary rate limit. (REST and GraphQL)
// https://docs.github.com/en/rest/overview/resources-in-the-rest-api#secondary-rate-limits
// The Retry-After header can sometimes be blank when hitting a secondary rate limit,
// but is always present after 2-3s, so make sure to set `retryAfter` to at least 5s by default.
const retryAfter = Number(error.response.headers["retry-after"]) ||
state.fallbackSecondaryRateRetryAfter;
const wantRetry = await emitter.trigger("secondary-limit", retryAfter, options, octokit, retryCount);
return { wantRetry, retryAfter };
}
if (error.response.headers != null &&
error.response.headers["x-ratelimit-remaining"] === "0") {
// The user has used all their allowed calls for the current time period (REST and GraphQL)
// https://docs.github.com/en/rest/reference/rate-limit (REST)
// https://docs.github.com/en/graphql/overview/resource-limitations#rate-limit (GraphQL)
const rateLimitReset = new Date(~~error.response.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, octokit, retryCount);
return { wantRetry, retryAfter };
}
return {};
})();
if (wantRetry) {
request.retryCount++;
return retryAfter * state.retryAfterBaseValue;
}
});
octokit.hook.wrap("request", wrapRequest.bind(null, state));
return {};
const retryCount = ~~request.retryCount;
request.retryCount = retryCount;
options.request.retryCount = retryCount;
const { wantRetry, retryAfter = 0 } = await async function() {
if (/\bsecondary rate\b/i.test(error.message)) {
const retryAfter2 = Number(error.response.headers["retry-after"]) || state2.fallbackSecondaryRateRetryAfter;
const wantRetry2 = await emitter.trigger(
"secondary-limit",
retryAfter2,
options,
octokit,
retryCount
);
return { wantRetry: wantRetry2, retryAfter: retryAfter2 };
}
if (error.response.headers != null && error.response.headers["x-ratelimit-remaining"] === "0") {
const rateLimitReset = new Date(
~~error.response.headers["x-ratelimit-reset"] * 1e3
).getTime();
const retryAfter2 = Math.max(
Math.ceil((rateLimitReset - Date.now()) / 1e3),
0
);
const wantRetry2 = await emitter.trigger(
"rate-limit",
retryAfter2,
options,
octokit,
retryCount
);
return { wantRetry: wantRetry2, retryAfter: retryAfter2 };
}
return {};
}();
if (wantRetry) {
request.retryCount++;
return retryAfter * state2.retryAfterBaseValue;
}
});
octokit.hook.wrap("request", wrapRequest.bind(null, state));
return {};
}
throttling.VERSION = VERSION;
throttling.triggersNotification = triggersNotification;
export { throttling };
//# sourceMappingURL=index.js.map
export {
throttling
};
{
"name": "@octokit/plugin-throttling",
"version": "6.0.1",
"publishConfig": {
"access": "public"
},
"description": "Octokit plugin for GitHub's recommended request throttling",
"version": "6.0.0",
"repository": "github:octokit/plugin-throttling.js",
"author": "Simon Grondin (http://github.com/SGrondin)",
"license": "MIT",
"files": [
"dist-*/**",
"bin/**"
],
"source": "dist-src/index.js",
"types": "dist-types/index.d.ts",
"main": "dist-node/index.js",
"module": "dist-web/index.js",
"pika": true,
"sideEffects": false,
"repository": "github:octokit/plugin-throttling.js",
"dependencies": {

@@ -27,11 +21,10 @@ "@octokit/types": "^9.0.0",

"@octokit/request-error": "^3.0.0",
"@pika/pack": "^0.3.7",
"@pika/plugin-build-node": "^0.9.1",
"@pika/plugin-build-web": "^0.9.1",
"@pika/plugin-ts-standard-pkg": "^0.9.1",
"@octokit/tsconfig": "^2.0.0",
"@types/fetch-mock": "^7.3.1",
"@types/jest": "^29.0.0",
"@types/node": "^18.0.0",
"esbuild": "^0.17.19",
"fetch-mock": "^9.0.0",
"github-openapi-graphql-query": "^4.0.0",
"glob": "^10.2.6",
"jest": "^29.0.0",

@@ -47,5 +40,11 @@ "npm-run-all": "^4.1.5",

},
"publishConfig": {
"access": "public"
}
"files": [
"dist-*/**",
"bin/**"
],
"main": "dist-node/index.js",
"browser": "dist-web/index.js",
"types": "dist-types/index.d.ts",
"module": "dist-src/index.js",
"sideEffects": false
}

@@ -156,4 +156,79 @@ # plugin-throttling.js

## Options
<table>
<thead align=left>
<tr>
<th>
name
</th>
<th>
type
</th>
<th width=100%>
description
</th>
</tr>
</thead>
<tbody align=left valign=top>
<tr>
<th>
<code>options.retryAfterBaseValue</code>
</th>
<td>
<code>Number</code>
</td>
<td>
Number of milliseconds that will be used to multiply the time to wait based on `retry-after` or `x-ratelimit-reset` headers. Defaults to <code>1000</code>
</td>
</tr>
<tr>
<th>
<code>options.fallbackSecondaryRateRetryAfter</code>
</th>
<td>
<code>Number</code>
</td>
<td>
Number of seconds to wait until retrying a request in case a secondary rate limit is hit and no <code>retry-after</code> header was present in the response. Defaults to <code>60</code>
</td>
</tr>
<tr>
<th>
<code>options.connection</code>
</th>
<td>
<code>Bottleneck.RedisConnection</code>
</td>
<td>
A Bottleneck connection instance. See <a href="#clustering">Clustering</a> above.
</td>
</tr>
<tr>
<th>
<code>options.id</code>
</th>
<td>
<code>string</code>
</td>
<td>
A "throttling ID". All octokit instances with the same ID using the same Redis server will share the throttling. See <a href="#clustering">Clustering</a> above. Defaults to <code>no-id</code>.
</td>
</tr>
<tr>
<th>
<code>options.Bottleneck</code>
</th>
<td>
<code>Bottleneck</code>
</td>
<td>
Bottleneck constructor. See <a href="#clustering">Clustering</a> above. Defaults to `bottleneck/light`.
</td>
</tr>
</tbody>
</table>
## LICENSE
[MIT](LICENSE)

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc