Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add timeout function #250

4 changes: 4 additions & 0 deletions .github/next-minor.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ https://github.com/radashi-org/radashi/pull/241

https://github.com/radashi-org/radashi/pull/305

#### timeout

https://github.com/radashi-org/radashi/pull/250

## New Features

#### Add `signal` option to `retry`
Expand Down
40 changes: 40 additions & 0 deletions docs/async/timeout.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: timeout
description: Create a promise that rejects after some time
since: 12.3.0
---

### Usage

The `timeout` function creates a promise that rejects after a specified delay, with an optional custom error message or error function.

The default error is a `TimeoutError` with the message "Operation timed out".

```ts
import * as _ from 'radashi'

// Rejects after 1 second with a default TimeoutError
await _.timeout(1000)

// Rejects after 1 second with a custom TimeoutError message
await _.timeout(1000, 'Custom timeout message')

// Rejects after 1 second with a custom error type
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')])
```
6 changes: 6 additions & 0 deletions src/async/TimeoutError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class TimeoutError extends Error {
name = 'TimeoutError'
constructor(message?: string) {
super(message ?? 'Operation timed out')
}
}
49 changes: 49 additions & 0 deletions src/async/timeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { isFunction } from 'radashi'
import { TimeoutError } from './TimeoutError'

declare const setTimeout: (fn: () => void, ms: number) => unknown

/**
* The `timeout` function creates a promise that rejects after a
* specified delay, with an optional custom error message or error
* function.
*
* @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"),
* ])
* ```
* @version 12.3.0
*/
export function timeout<TError extends Error>(
/**
* The number of milliseconds to wait before rejecting.
*/
ms: number,
/**
* An error message or a function that returns an error. By default,
* a `TimeoutError` is thrown with the message "Operation timed
* out".
*/
error?: string | (() => TError),
): Promise<never> {
return new Promise((_, reject) =>
setTimeout(
() => reject(isFunction(error) ? error() : new TimeoutError(error)),
ms,
),
)
}
2 changes: 2 additions & 0 deletions src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ 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/TimeoutError.ts'
export * from './async/tryit.ts'
export * from './async/withResolvers.ts'

Expand Down
51 changes: 51 additions & 0 deletions tests/async/timeout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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(_.TimeoutError)
})

test('rejects with a custom error message', async () => {
const promise = _.timeout(10, 'too slow')

vi.advanceTimersToNextTimerAsync()

await expect(promise).rejects.toThrow(new _.TimeoutError('too slow'))
})

test('rejects with a custom error function', async () => {
class CustomError extends Error {}

const promise = _.timeout(10, () => new CustomError())

vi.advanceTimersToNextTimerAsync()

await expect(promise).rejects.toThrow(CustomError)
})

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()
})
})
})
Loading