|
1 | 1 | const url = require('url'); |
2 | | -const GitHubApi = require('@octokit/rest'); |
| 2 | +const {memoize} = require('lodash'); |
| 3 | +const Octokit = require('@octokit/rest'); |
| 4 | +const pRetry = require('p-retry'); |
| 5 | +const pThrottle = require('p-throttle'); |
3 | 6 |
|
4 | | -module.exports = (githubToken, githubUrl, githubApiPathPrefix) => { |
| 7 | +/** |
| 8 | + * Default exponential backoff configuration for retries. |
| 9 | + */ |
| 10 | +const DEFAULT_RETRY = {retries: 3, factor: 2, minTimeout: 1000}; |
| 11 | + |
| 12 | +/** |
| 13 | + * Rate limit per API endpoints. |
| 14 | + * |
| 15 | + * See {@link https://developer.github.com/v3/search/#rate-limit|Search API rate limit}. |
| 16 | + * See {@link https://developer.github.com/v3/#rate-limiting|Rate limiting}. |
| 17 | + */ |
| 18 | +const RATE_LIMITS = { |
| 19 | + search: [30, 60 * 1000], |
| 20 | + core: [5000, 60 * 60 * 1000], |
| 21 | +}; |
| 22 | + |
| 23 | +/** |
| 24 | + * Http error codes for which to not retry. |
| 25 | + */ |
| 26 | +const SKIP_RETRY_CODES = [400, 401, 403]; |
| 27 | + |
| 28 | +/** |
| 29 | + * @typedef {Function} Throttler |
| 30 | + * @param {Function} func The function to throttle. |
| 31 | + * @param {Arguments} args The arguments to pass to the function to throttle. |
| 32 | + */ |
| 33 | + |
| 34 | +/** |
| 35 | + * Create or retrieve the throller function for a given rate limit group. |
| 36 | + * |
| 37 | + * @param {Array} rate The rate limit group. |
| 38 | + * @param {String} limit The rate limits per API endpoints. |
| 39 | + * @type {Throttler} The throller function for the given rate limit group. |
| 40 | + */ |
| 41 | +const getThrottler = memoize((rate, limit) => pThrottle((func, ...args) => func(...args), ...limit[rate])); |
| 42 | + |
| 43 | +/** |
| 44 | + * Create a`handler` for a `Proxy` wrapping an Octokit instance to: |
| 45 | + * - Recursively wrap the child objects of the Octokit instance in a `Proxy` |
| 46 | + * - Throttle and retry the Octokit instance functions |
| 47 | + * |
| 48 | + * @param {Object} retry The configuration to pass to `p-retry`. |
| 49 | + * @param {Array} limit The rate limits per API endpoints. |
| 50 | + * @param {String} endpoint The API endpoint to handle. |
| 51 | + * @return {Function} The `handler` for a `Proxy` wrapping an Octokit instance. |
| 52 | + */ |
| 53 | +const handler = (retry, limit, endpoint) => ({ |
| 54 | + /** |
| 55 | + * If the target has the property as own, determine the rate limit based on the property name and recursively wrap the value in a `Proxy`. Otherwise returns the property value. |
| 56 | + * |
| 57 | + * @param {Object} target The target object. |
| 58 | + * @param {String} name The name of the property to get. |
| 59 | + * @param {Any} receiver The `Proxy` object. |
| 60 | + * @return {Any} The property value or a `Proxy` of the property value. |
| 61 | + */ |
| 62 | + get: (target, name, receiver) => |
| 63 | + Object.prototype.hasOwnProperty.call(target, name) |
| 64 | + ? new Proxy(target[name], handler(retry, limit, endpoint || name)) |
| 65 | + : Reflect.get(target, name, receiver), |
| 66 | + |
| 67 | + /** |
| 68 | + * Create a throlled version of the called function tehn call it and retry it if the call fails with certain error code. |
| 69 | + * |
| 70 | + * @param {Function} func The target function. |
| 71 | + * @param {Any} that The this argument for the call. |
| 72 | + * @param {Array} args The list of arguments for the call. |
| 73 | + * @return {Promise<Any>} The result of the function called. |
| 74 | + */ |
| 75 | + apply: (func, that, args) => { |
| 76 | + const throttler = getThrottler(limit[endpoint] ? endpoint : 'core', limit); |
| 77 | + return pRetry(async () => { |
| 78 | + try { |
| 79 | + return await throttler(func, ...args); |
| 80 | + } catch (err) { |
| 81 | + if (SKIP_RETRY_CODES.includes(err.code)) { |
| 82 | + throw new pRetry.AbortError(err); |
| 83 | + } |
| 84 | + throw err; |
| 85 | + } |
| 86 | + }, retry); |
| 87 | + }, |
| 88 | +}); |
| 89 | + |
| 90 | +module.exports = ({githubToken, githubUrl, githubApiPathPrefix, retry = DEFAULT_RETRY, limit = RATE_LIMITS}) => { |
5 | 91 | const {port, protocol, hostname} = githubUrl ? url.parse(githubUrl) : {}; |
6 | | - const github = new GitHubApi({ |
| 92 | + const github = new Octokit({ |
7 | 93 | port, |
8 | 94 | protocol: (protocol || '').split(':')[0] || null, |
9 | 95 | host: hostname, |
10 | 96 | pathPrefix: githubApiPathPrefix, |
11 | 97 | }); |
12 | 98 | github.authenticate({type: 'token', token: githubToken}); |
13 | | - return github; |
| 99 | + return new Proxy(github, handler(retry, limit)); |
14 | 100 | }; |
0 commit comments