diff --git a/benchmarks/async/timeout.bench.ts b/benchmarks/async/timeout.bench.ts new file mode 100644 index 00000000..4186d717 --- /dev/null +++ b/benchmarks/async/timeout.bench.ts @@ -0,0 +1,21 @@ +import * as _ from 'radashi' + +describe('timeout', () => { + bench('with default error message', async () => { + try { + await _.timeout(0) + } catch (_) {} + }) + + bench('with custom error message', async () => { + try { + await _.timeout(0, 'Optional message') + } catch (_) {} + }) + + bench('with custom error function', async () => { + try { + await _.timeout(0, () => new Error('Custom error')) + } catch (_) {} + }) +}) diff --git a/docs/async/timeout.mdx b/docs/async/timeout.mdx new file mode 100644 index 00000000..c80590c1 --- /dev/null +++ b/docs/async/timeout.mdx @@ -0,0 +1,37 @@ +--- +title: timeout +description: Asynchronously rejects after a specified amount of time. +--- + +### Usage + +The `_.timeout` function creates a promise that will reject after a given number of milliseconds. You can provide a custom error message or a function that returns an error to be thrown upon rejection. + +```ts +import * as _ from 'radashi' + +// Rejects after 1 second with the default "timeout" error message +await _.timeout(1000) + +// Rejects after 1 second with a custom error message +await _.timeout(1000, 'Custom timeout message') + +// Rejects after 1 second with a custom error object +await _.timeout(1000, () => new Error('Custom error')) +``` + +### Example with `Promise.race` + +One of the most useful ways to use `_.timeout` with `Promise.race` to set a timeout for an asynchronous operation. + +```ts +import * as _ from 'radashi' + +const someAsyncTask = async () => { + await _.sleep(10_000) + return 'Task completed' +} + +// Race between the async task and a timeout of 1 second +await Promise.race([someAsyncTask(), _.timeout(1000, 'Task took too long')]) +``` diff --git a/src/async/timeout.ts b/src/async/timeout.ts new file mode 100644 index 00000000..5b5c81b4 --- /dev/null +++ b/src/async/timeout.ts @@ -0,0 +1,48 @@ +import { isString } from 'radashi' + +declare const setTimeout: (fn: () => void, ms: number) => unknown + +/** + * Creates a promise that will reject after a specified amount of time. + * You can provide a custom error message or a function that returns an error. + * + * @see https://radashi.js.org/reference/async/timeout + * + * @example + * ```ts + * // Reject after 1000 milliseconds with default message "timeout" + * await timeout(1000) + * + * // Reject after 1000 milliseconds with a custom message + * await timeout(1000, "Optional message") + * + * // Reject after 1000 milliseconds with a custom error + * await timeout(1000, () => new Error("Custom error")) + * + * // Example usage with Promise.race to set a timeout for an asynchronous task + * await Promise.race([ + * someAsyncTask(), + * timeout(1000, "Optional message"), + * ]) + * ``` + */ + +export function timeout( + milliseconds: number, + /** + * The error message to reject with. + * + * @default "timeout" + */ + error: string | (() => E) = 'timeout', +): Promise { + return new Promise((_, rej) => + setTimeout(() => { + if (isString(error)) { + rej(new Error(error)) + } else { + rej(error()) + } + }, milliseconds), + ) +} diff --git a/src/mod.ts b/src/mod.ts index ae241665..03da6ee4 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -38,6 +38,7 @@ export * from './async/parallel.ts' export * from './async/reduce.ts' export * from './async/retry.ts' export * from './async/sleep.ts' +export * from './async/timeout.ts' export * from './async/tryit.ts' export * from './async/withResolvers.ts' diff --git a/tests/async/timout.test.ts b/tests/async/timout.test.ts new file mode 100644 index 00000000..a464c0ff --- /dev/null +++ b/tests/async/timout.test.ts @@ -0,0 +1,56 @@ +import * as _ from 'radashi' + +describe('timeout', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + test('rejects after a specified number of milliseconds', async () => { + const promise = _.timeout(10) + + vi.advanceTimersToNextTimerAsync() + + await expect(promise).rejects.toThrow('timeout') + }) + + test('rejects with a custom error message', async () => { + const promise = _.timeout(10, 'custom error message') + + vi.advanceTimersToNextTimerAsync() + + await expect(promise).rejects.toThrow('custom error message') + }) + + test('rejects with a custom error function', async () => { + class CustomError extends Error { + constructor() { + super('custom error function') + } + } + + const promise = _.timeout(10, () => new CustomError()) + + vi.advanceTimersToNextTimerAsync() + + await expect(promise).rejects.toThrow(CustomError) + await expect(promise).rejects.toThrow('custom error function') + }) + + describe('with Promise.race', () => { + test('resolves correctly when sleep finishes before timeout', async () => { + const promise = Promise.race([_.sleep(10), _.timeout(100)]) + + vi.advanceTimersByTime(100) + + await expect(promise).resolves.toBeUndefined() + }) + + test('rejects with timeout when it finishes before sleep', async () => { + const promise = Promise.race([_.sleep(100), _.timeout(10)]) + + vi.advanceTimersByTime(100) + + await expect(promise).rejects.toThrow() + }) + }) +})