@octokit/plugin-throttling
Advanced tools
Comparing version 6.0.0 to 6.0.1
@@ -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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
Found 1 instance in 1 package
16
698
1
234
56807
18