'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }

var BottleneckLight = _interopDefault(require('bottleneck/light'));

const VERSION = "5.2.3";

const noop = () => Promise.resolve();
// @ts-expect-error
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;
  }
  // 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;
}

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"];

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");
}

// @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
  });
};
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);
  }
  if (octokitOptions.throttle && octokitOptions.throttle.minimalSecondaryRateRetryAfter) {
    octokit.log.warn("[@octokit/plugin-throttling] `options.throttle.minimalSecondaryRateRetryAfter` is deprecated, please use `options.throttle.fallbackSecondaryRateRetryAfter` instead");
    octokitOptions.throttle.fallbackSecondaryRateRetryAfter = octokitOptions.throttle.minimalSecondaryRateRetryAfter;
    delete octokitOptions.throttle.minimalSecondaryRateRetryAfter;
  }
  if (octokitOptions.throttle && octokitOptions.throttle.onAbuseLimit) {
    octokit.log.warn("[@octokit/plugin-throttling] `onAbuseLimit()` is deprecated and will be removed in a future release of `@octokit/plugin-throttling`, please use the `onSecondaryRateLimit` handler instead");
    // @ts-ignore types don't allow for both properties to be set
    octokitOptions.throttle.onSecondaryRateLimit = octokitOptions.throttle.onAbuseLimit;
    // @ts-ignore
    delete octokitOptions.throttle.onAbuseLimit;
  }
  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:
        You must pass the onSecondaryRateLimit and onRateLimit error handlers.
        See https://octokit.github.io/rest.js/#throttling

        const octokit = new Octokit({
          throttle: {
            onSecondaryRateLimit: (retryAfter, options) => {/* ... */},
            onRateLimit: (retryAfter, options) => {/* ... */}
          }
        })
    `);
  }
  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 {};
}
throttling.VERSION = VERSION;
throttling.triggersNotification = triggersNotification;

exports.throttling = throttling;
//# sourceMappingURL=index.js.map
