From b493d7273949aac23436cb446ee426717cd89d2f Mon Sep 17 00:00:00 2001 From: jlenon7 Date: Sat, 28 Mar 2026 21:43:04 -0300 Subject: [PATCH] feat(timeout): add helper to race promises with timeout --- package-lock.json | 4 +- package.json | 2 +- src/helpers/Timeout.ts | 143 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 src/helpers/Timeout.ts diff --git a/package-lock.json b/package-lock.json index 7aa1dcf..da863bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/common", - "version": "5.32.0", + "version": "5.33.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athenna/common", - "version": "5.32.0", + "version": "5.33.0", "license": "MIT", "dependencies": { "@fastify/formbody": "^8.0.2", diff --git a/package.json b/package.json index 7c63d27..9255da7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/common", - "version": "5.32.0", + "version": "5.33.0", "description": "The Athenna common helpers to use in any Node.js ESM project.", "license": "MIT", "author": "João Lenon ", diff --git a/src/helpers/Timeout.ts b/src/helpers/Timeout.ts new file mode 100644 index 0000000..cb5fd9b --- /dev/null +++ b/src/helpers/Timeout.ts @@ -0,0 +1,143 @@ +/** + * @athenna/common + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + + +export class TimeoutBuilder { + private promise: Promise; + + private options: { + ms?: number; + onTimeout?: () => any; + onError?: (error: Error) => any; + }; + + public constructor(promise: Promise) { + this.options = {}; + this.promise = promise; + } + + /** + * Define the timeout in milliseconds. + * + * @example + * ```ts + * const result = await Timeout.when(promise) + * .ms(1000) + * .race() + * ``` + */ + public ms(ms: number) { + this.options.ms = ms; + + return this; + } + + /** + * Define the callback to be executed when an error occurs handling + * the promise. + * + * @example + * ```ts + * const result = await Timeout.when(promise) + * .ms(1000) + * .onError(error => ('fallback error')) + * .race() + * ``` + */ + public onError(closure: (error: Error) => Promise) { + this.options.onError = closure; + + return this; + } + + /** + * Define the callback to be executed when the timeout occurs. + * + * @example + * ```ts + * const result = await Timeout.when(promise) + * .ms(1000) + * .onTimeout(() => ('fallback timeout')) + * .race() + * ``` + */ + public onTimeout(closure: () => any) { + this.options.onTimeout = closure; + + return this; + } + + /** + * Race the promise with timeout. + * + * @example + * ```ts + * const result = await Timeout.when(promise) + * .ms(1000) + * .onTimeout(() => ('fallback timeout')) + * .onError(error => ('fallback error')) + * .race() + * ``` + */ + public async race() { + if (!this.options.ms) { + throw new Error("ms is required"); + } + + if (!this.options.onTimeout) { + throw new Error("onTimeout is required"); + } + + let timeoutId: NodeJS.Timeout; + + const timeout = new Promise((_, reject) => { + timeoutId = setTimeout( + () => reject(new Error("__RaceTimeout__")), + this.options.ms, + ); + }); + + return Promise.race([this.promise, timeout]) + .then((result) => { + clearTimeout(timeoutId); + return result; + }) + .catch((error) => { + clearTimeout(timeoutId); + + if (error.message === "__RaceTimeout__" && this.options.onTimeout) { + return this.options.onTimeout(); + } + + if (this.options.onError) { + return this.options.onError(error); + } + + throw error; + }); + } +} + +export class Timeout { + /** + * Create a new timeout builder. + * + * @example + * ```ts + * const result = await Timeout.when(promise) + * .ms(1000) + * .onTimeout(() => ('fallback timeout')) + * .onError(error => ('fallback error')) + * .race() + * ``` + */ + public static when(promise: Promise) { + return new TimeoutBuilder(promise); + } +}