unstructured-client
Version:
<h3 align="center"> <img src="https://raw.githubusercontent.com/Unstructured-IO/unstructured/main/img/unstructured_logo.png" height="200" > </h3>
219 lines (183 loc) • 5.23 kB
text/typescript
/*
* Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
*/
import { isConnectionError, isTimeoutError } from "./http.js";
export type BackoffStrategy = {
initialInterval: number;
maxInterval: number;
exponent: number;
maxElapsedTime: number;
};
const defaultBackoff: BackoffStrategy = {
initialInterval: 500,
maxInterval: 60000,
exponent: 1.5,
maxElapsedTime: 3600000,
};
export type RetryConfig =
| { strategy: "none" }
| {
strategy: "backoff";
backoff?: BackoffStrategy;
retryConnectionErrors?: boolean;
};
/**
* PermanentError is an error that is not recoverable. Throwing this error will
* cause a retry loop to terminate.
*/
export class PermanentError extends Error {
/** The underlying cause of the error. */
override readonly cause: unknown;
constructor(message: string, options?: { cause?: unknown }) {
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);
}
}
/**
* 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.
*/
export class TemporaryError extends Error {
response: Response;
constructor(message: string, response: Response) {
super(message);
this.response = response;
this.name = "TemporaryError";
Object.setPrototypeOf(this, TemporaryError.prototype);
}
}
export async function retry(
fetchFn: () => Promise<Response>,
options: {
config: RetryConfig;
statusCodes: string[];
},
): Promise<Response> {
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: () => Promise<Response>,
options: {
statusCodes: string[];
retryConnectionErrors: boolean;
},
): () => Promise<Response> {
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: unknown) {
if (err instanceof TemporaryError) {
throw err;
}
if (
options.retryConnectionErrors &&
(isTimeoutError(err) || isConnectionError(err))
) {
throw err;
}
throw new PermanentError("Permanent error", { cause: err });
}
};
}
const codeRangeRE = new RegExp("^[0-9]xx$", "i");
function isRetryableResponse(res: Response, statusCodes: string[]): boolean {
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: () => Promise<Response>,
strategy: BackoffStrategy,
): Promise<Response> {
const { maxElapsedTime, initialInterval, exponent, maxInterval } = strategy;
const start = Date.now();
let x = 0;
while (true) {
try {
const res = await fn();
return res;
} catch (err: unknown) {
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: Response): number {
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: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, delay));
}