153 lines
4.9 KiB
JavaScript
153 lines
4.9 KiB
JavaScript
"use strict";
|
|
/*
|
|
* Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
|
|
*/
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.TemporaryError = exports.PermanentError = void 0;
|
|
exports.retry = retry;
|
|
const http_js_1 = require("./http.js");
|
|
const defaultBackoff = {
|
|
initialInterval: 500,
|
|
maxInterval: 60000,
|
|
exponent: 1.5,
|
|
maxElapsedTime: 3600000,
|
|
};
|
|
/**
|
|
* PermanentError is an error that is not recoverable. Throwing this error will
|
|
* cause a retry loop to terminate.
|
|
*/
|
|
class PermanentError extends Error {
|
|
constructor(message, options) {
|
|
let msg = message;
|
|
if (options?.cause) {
|
|
msg += `: ${options.cause}`;
|
|
}
|
|
super(msg, options);
|
|
this.name = "PermanentError";
|
|
// In older runtimes, the cause field would not have been assigned through
|
|
// the super() call.
|
|
if (typeof this.cause === "undefined") {
|
|
this.cause = options?.cause;
|
|
}
|
|
Object.setPrototypeOf(this, PermanentError.prototype);
|
|
}
|
|
}
|
|
exports.PermanentError = PermanentError;
|
|
/**
|
|
* TemporaryError is an error is used to signal that an HTTP request can be
|
|
* retried as part of a retry loop. If retry attempts are exhausted and this
|
|
* error is thrown, the response will be returned to the caller.
|
|
*/
|
|
class TemporaryError extends Error {
|
|
constructor(message, response) {
|
|
super(message);
|
|
this.response = response;
|
|
this.name = "TemporaryError";
|
|
Object.setPrototypeOf(this, TemporaryError.prototype);
|
|
}
|
|
}
|
|
exports.TemporaryError = TemporaryError;
|
|
async function retry(fetchFn, options) {
|
|
switch (options.config.strategy) {
|
|
case "backoff":
|
|
return retryBackoff(wrapFetcher(fetchFn, {
|
|
statusCodes: options.statusCodes,
|
|
retryConnectionErrors: !!options.config.retryConnectionErrors,
|
|
}), options.config.backoff ?? defaultBackoff);
|
|
default:
|
|
return await fetchFn();
|
|
}
|
|
}
|
|
function wrapFetcher(fn, options) {
|
|
return async () => {
|
|
try {
|
|
const res = await fn();
|
|
if (isRetryableResponse(res, options.statusCodes)) {
|
|
throw new TemporaryError("Response failed with retryable status code", res);
|
|
}
|
|
return res;
|
|
}
|
|
catch (err) {
|
|
if (err instanceof TemporaryError) {
|
|
throw err;
|
|
}
|
|
if (options.retryConnectionErrors &&
|
|
((0, http_js_1.isTimeoutError)(err) || (0, http_js_1.isConnectionError)(err))) {
|
|
throw err;
|
|
}
|
|
throw new PermanentError("Permanent error", { cause: err });
|
|
}
|
|
};
|
|
}
|
|
const codeRangeRE = new RegExp("^[0-9]xx$", "i");
|
|
function isRetryableResponse(res, statusCodes) {
|
|
const actual = `${res.status}`;
|
|
return statusCodes.some((code) => {
|
|
if (!codeRangeRE.test(code)) {
|
|
return code === actual;
|
|
}
|
|
const expectFamily = code.charAt(0);
|
|
if (!expectFamily) {
|
|
throw new Error("Invalid status code range");
|
|
}
|
|
const actualFamily = actual.charAt(0);
|
|
if (!actualFamily) {
|
|
throw new Error(`Invalid response status code: ${actual}`);
|
|
}
|
|
return actualFamily === expectFamily;
|
|
});
|
|
}
|
|
async function retryBackoff(fn, strategy) {
|
|
const { maxElapsedTime, initialInterval, exponent, maxInterval } = strategy;
|
|
const start = Date.now();
|
|
let x = 0;
|
|
while (true) {
|
|
try {
|
|
const res = await fn();
|
|
return res;
|
|
}
|
|
catch (err) {
|
|
if (err instanceof PermanentError) {
|
|
throw err.cause;
|
|
}
|
|
const elapsed = Date.now() - start;
|
|
if (elapsed > maxElapsedTime) {
|
|
if (err instanceof TemporaryError) {
|
|
return err.response;
|
|
}
|
|
throw err;
|
|
}
|
|
let retryInterval = 0;
|
|
if (err instanceof TemporaryError) {
|
|
retryInterval = retryIntervalFromResponse(err.response);
|
|
}
|
|
if (retryInterval <= 0) {
|
|
retryInterval =
|
|
initialInterval * Math.pow(x, exponent) + Math.random() * 1000;
|
|
}
|
|
const d = Math.min(retryInterval, maxInterval);
|
|
await delay(d);
|
|
x++;
|
|
}
|
|
}
|
|
}
|
|
function retryIntervalFromResponse(res) {
|
|
const retryVal = res.headers.get("retry-after") || "";
|
|
if (!retryVal) {
|
|
return 0;
|
|
}
|
|
const parsedNumber = Number(retryVal);
|
|
if (Number.isInteger(parsedNumber)) {
|
|
return parsedNumber * 1000;
|
|
}
|
|
const parsedDate = Date.parse(retryVal);
|
|
if (Number.isInteger(parsedDate)) {
|
|
const deltaMS = parsedDate - Date.now();
|
|
return deltaMS > 0 ? Math.ceil(deltaMS) : 0;
|
|
}
|
|
return 0;
|
|
}
|
|
async function delay(delay) {
|
|
return new Promise((resolve) => setTimeout(resolve, delay));
|
|
}
|
|
//# sourceMappingURL=retries.js.map
|