UNPKG

@clickup/rest-client

Version:

A syntax sugar tool around Node fetch() API, tailored to work with TypeScript and response validators

190 lines (182 loc) 7.44 kB
import { Agent as HttpAgent } from "http"; import { Agent as HttpsAgent } from "https"; import delay from "delay"; import { Memoize } from "fast-typescript-memoize"; import type RestRequest from "./RestRequest"; import type RestResponse from "./RestResponse"; /** * An event which is passed to an external logger (see RestOptions). */ export interface RestLogEvent { attempt: number; req: RestRequest; res: RestResponse | "backoff_delay" | null; exception: any | null; timestamp: number; elapsed: number; isFinalAttempt: boolean; privateDataInResponse: boolean; comment: string; } /** * Middlewares allow to modify RestRequest and RestResponse objects during the * request processing. */ export interface Middleware { ( req: RestRequest, next: (req: RestRequest) => Promise<RestResponse>, ): Promise<RestResponse>; } /** * @ignore * Parameters for Agents. */ interface AgentOptions { keepAlive: boolean; timeout?: number; maxSockets?: number; rejectUnauthorized?: boolean; family?: 4 | 6 | 0; } /** * @ignore * An internal class which keeps the list of HttpAgent instances used in a * particular RestClient instance. */ export class Agents { @Memoize((...args: any[]) => JSON.stringify(args)) http(options: AgentOptions) { return new HttpAgent(options); } @Memoize((...args: any[]) => JSON.stringify(args)) https(options: AgentOptions) { return new HttpsAgent(options); } } /** * Options passed to RestClient. More options can be added by cloning an * instance of RestClient, withOption() method. */ export default interface RestOptions { /** Max number of retries. Default is 0, because some requests are from the * web app, and we don't want to retry them. */ retries: number; /** How much time to wait by default on the 1st retry attempt. */ retryDelayFirstMs: number; /** How much to increase the retry delay on each retry. */ retryDelayFactor: number; /** Use this fraction (random) of the current retry delay to jitter both ways * (e.g. 0.1 means 90%...110% of the delay to be actually applied). */ retryDelayJitter: number; /** Maximum delay between each retry. */ retryDelayMaxMs: number; /** A logic which runs on different IO stages (delay and heartbeats). */ heartbeater: { /* This function is called after each request with 0 passed, so it can serve * the purpose of a heartbeat. */ heartbeat(): Promise<void>; /** A function which, when runs, resolves in the provided number of ms. Can * be used for several purposes, like overriding in unit tests or to pass a * custom delay implementation which can throw on some external event (like * when the process wants to gracefully stop). */ delay(ms: number): Promise<void>; }; /** Allows to limit huge requests and throw instead. */ throwIfResIsBigger: number | undefined; /** Passed to the logger which may decide, should it log details of the * response or not. */ privateDataInResponse: boolean; /** If true, non-public IP addresses are allowed too; otherwise, only unicast * addresses are allowed. */ allowInternalIPs: boolean; /** Overrides the default encoding heuristics for responses. */ responseEncoding: NodeJS.BufferEncoding | undefined; /** If true, logs request-response pairs to console. */ isDebug: boolean; /** @ignore Holds HttpsAgent/HttpAgent instances; used internally only. */ agents: Agents; /** Sets Keep-Alive parameters (persistent connections). */ keepAlive: { /** How much time to keep an idle connection alive in the pool. If 0, closes * the connection immediately after the response. */ timeoutMs: number; /** How many sockets at maximum will be kept open. */ maxSockets?: number; }; /** When resolving DNS, use IPv4, IPv6 or both (see dns.lookup() docs). */ family: 4 | 6 | 0; /** Max timeout to wait for a response. */ timeoutMs: number; /** Logger to be used for each responses (including retried) plus for backoff * delay events logging. */ logger: (event: RestLogEvent) => void; /** Middlewares to wrap requests. May alter both request and response. */ middlewares: Middleware[]; /** If set, makes decision whether the response is successful or not. The * response will either be returned to the client, or an error will be thrown. * This allows to treat some non-successful HTTP statuses as success if the * remote API is that weird. Return values: * * "SUCCESS" - the request will be considered successful, no further checks * will be performed; * * "BEST_EFFORT" - inconclusive, the request may be either successful or * unsuccessful, additional tests (e.g. will check HTTP status code) will be * performed; * * "THROW" - the request resulted in error. Additional tests will be * performed to determine is the error is retriable, is OAuth token good, * and etc. */ isSuccessResponse: (res: RestResponse) => "SUCCESS" | "THROW" | "BEST_EFFORT"; /** Decides whether the response is a rate-limit error or not. Returning * non-zero value is treated as retry delay (if retries are set up). In case * the returned value is "SOMETHING_ELSE", the response ought to be either * success or some other error. Returning "BEST_EFFORT" turns on built-in * heuristic (e.g. relying on HTTP status code and Retry-After header). In * case we've made a decision that it's a rate limited error, the request is * always retried; this covers a very common case when we have both * isRateLimitError and isRetriableError handlers set up, and they return * contradictory information; then isRateLimitError wins. */ isRateLimitError: ( res: RestResponse, ) => "SOMETHING_ELSE" | "RATE_LIMIT" | "BEST_EFFORT" | number; /** Decides whether the response is a token-invalid error or not. In case it's * not, the response ought to be either success or some other error. */ isTokenInvalidError: (res: RestResponse) => boolean; /** Called only if we haven't decided earlier that it's a rate limit error. * Decides whether the response is a retriable error or not. In case the * returned value is "NEVER_RETRY", the response ought to be either success or * some other error, but it's guaranteed that the request won't be retried. * Returning "BEST_EFFORT" turns on built-in heuristics (e.g. never retry "not * found" errors). Returning a number is treated as "RETRY", and the next * retry will happen in not less than this number of milliseconds. */ isRetriableError: ( res: RestResponse, _error: any, ) => "NEVER_RETRY" | "RETRY" | "BEST_EFFORT" | number; } /** @ignore */ export const DEFAULT_OPTIONS: RestOptions = { retries: 0, retryDelayFirstMs: 1000, retryDelayFactor: 2, retryDelayJitter: 0.1, retryDelayMaxMs: Number.MAX_SAFE_INTEGER, heartbeater: { heartbeat: async () => {}, delay, }, throwIfResIsBigger: undefined, privateDataInResponse: false, allowInternalIPs: false, responseEncoding: undefined, isDebug: false, agents: new Agents(), keepAlive: { timeoutMs: 10000 }, family: 4, // we don't want to hit ~5s DNS timeout on a missing IPv6 by default timeoutMs: 4 * 60 * 1000, logger: () => {}, middlewares: [], isSuccessResponse: () => "BEST_EFFORT", isRateLimitError: () => "BEST_EFFORT", isTokenInvalidError: () => false, isRetriableError: () => "BEST_EFFORT", };