Spaces:
Build error
Build error
import pRetry from "p-retry"; | |
import PQueueMod from "p-queue"; | |
const STATUS_NO_RETRY = [ | |
400, | |
401, | |
403, | |
404, | |
405, | |
406, | |
407, | |
408, | |
409, // Conflict | |
]; | |
/** | |
* A class that can be used to make async calls with concurrency and retry logic. | |
* | |
* This is useful for making calls to any kind of "expensive" external resource, | |
* be it because it's rate-limited, subject to network issues, etc. | |
* | |
* Concurrent calls are limited by the `maxConcurrency` parameter, which defaults | |
* to `Infinity`. This means that by default, all calls will be made in parallel. | |
* | |
* Retries are limited by the `maxRetries` parameter, which defaults to 6. This | |
* means that by default, each call will be retried up to 6 times, with an | |
* exponential backoff between each attempt. | |
*/ | |
export class AsyncCaller { | |
constructor(params) { | |
Object.defineProperty(this, "maxConcurrency", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
Object.defineProperty(this, "maxRetries", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
Object.defineProperty(this, "queue", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
this.maxConcurrency = params.maxConcurrency ?? Infinity; | |
this.maxRetries = params.maxRetries ?? 6; | |
const PQueue = "default" in PQueueMod ? PQueueMod.default : PQueueMod; | |
this.queue = new PQueue({ concurrency: this.maxConcurrency }); | |
} | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
call(callable, ...args) { | |
return this.queue.add(() => pRetry(() => callable(...args).catch((error) => { | |
// eslint-disable-next-line no-instanceof/no-instanceof | |
if (error instanceof Error) { | |
throw error; | |
} | |
else { | |
throw new Error(error); | |
} | |
}), { | |
onFailedAttempt(error) { | |
if (error.message.startsWith("Cancel") || | |
error.message.startsWith("TimeoutError") || | |
error.message.startsWith("AbortError")) { | |
throw error; | |
} | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
if (error?.code === "ECONNABORTED") { | |
throw error; | |
} | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
const status = error?.response?.status; | |
if (status && STATUS_NO_RETRY.includes(+status)) { | |
throw error; | |
} | |
}, | |
retries: this.maxRetries, | |
randomize: true, | |
// If needed we can change some of the defaults here, | |
// but they're quite sensible. | |
}), { throwOnTimeout: true }); | |
} | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
callWithOptions(options, callable, ...args) { | |
// Note this doesn't cancel the underlying request, | |
// when available prefer to use the signal option of the underlying call | |
if (options.signal) { | |
return Promise.race([ | |
this.call(callable, ...args), | |
new Promise((_, reject) => { | |
options.signal?.addEventListener("abort", () => { | |
reject(new Error("AbortError")); | |
}); | |
}), | |
]); | |
} | |
return this.call(callable, ...args); | |
} | |
fetch(...args) { | |
return this.call(() => fetch(...args).then((res) => (res.ok ? res : Promise.reject(res)))); | |
} | |
} | |