Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions async/unstable_deadline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2018-2025 the Deno authors. MIT license.
// This module is browser compatible.

import { abortable } from "./abortable.ts";

/** Options for {@linkcode deadline}. */
export interface DeadlineOptions {
/** Signal used to abort the deadline. */
signal?: AbortSignal;
/**
* Allow the deadline to be infinite.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*/
allowInfinity?: boolean;
}

/**
* Create a promise which will be rejected with {@linkcode DOMException} when
* a given delay is exceeded.
*
* Note: Prefer to use {@linkcode AbortSignal.timeout} instead for the APIs
* that accept {@linkcode AbortSignal}.
*
* @throws {DOMException & { name: "TimeoutError" }} If the provided duration
* runs out before resolving.
* @throws {DOMException & { name: "AbortError" }} If the optional signal is
* aborted with the default `reason` before resolving or timing out.
* @throws {AbortSignal["reason"]} If the optional signal is aborted with a
* custom `reason` before resolving or timing out.
* @typeParam T The type of the provided and returned promise.
* @param p The promise to make rejectable.
* @param ms Duration in milliseconds for when the promise should time out.
* @param options Additional options.
* @returns A promise that will reject if the provided duration runs out before resolving.
*
* @example Usage
* ```ts ignore
* import { deadline } from "@std/async/deadline";
* import { delay } from "@std/async/delay";
*
* const delayedPromise = delay(1_000);
* // Below throws `DOMException` after 10 ms
* const result = await deadline(delayedPromise, 10);
* ```
*/
export async function deadline<T>(
p: Promise<T>,
ms: number,
options: DeadlineOptions = {},
): Promise<T> {
const signals: AbortSignal[] = [];
if ((!options?.allowInfinity) || (ms !== Infinity)) {
signals.push(AbortSignal.timeout(ms));
}
if (options.signal) signals.push(options.signal);
return await abortable(p, AbortSignal.any(signals));
}
102 changes: 102 additions & 0 deletions async/unstable_deadline_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright 2018-2025 the Deno authors. MIT license.
import { assertEquals, assertRejects } from "@std/assert";
import { delay } from "./delay.ts";
import { deadline } from "./unstable_deadline.ts";

Deno.test("deadline() returns fulfilled promise", async () => {
const controller = new AbortController();
const { signal } = controller;
const p = delay(100, { signal })
.catch(() => {})
.then(() => "Hello");
const result = await deadline(p, 1000);
assertEquals(result, "Hello");
controller.abort();
});

Deno.test("deadline() throws DOMException", async () => {
const controller = new AbortController();
const { signal } = controller;
const p = delay(1000, { signal })
.catch(() => {})
.then(() => "Hello");
const error = await assertRejects(
() => deadline(p, 100),
DOMException,
"Signal timed out.",
);
assertEquals(error.name, "TimeoutError");
controller.abort();
});

Deno.test("deadline() throws when promise is rejected", async () => {
const controller = new AbortController();
const { signal } = controller;
const p = delay(100, { signal })
.catch(() => {})
.then(() => Promise.reject(new Error("booom")));
await assertRejects(
async () => {
await deadline(p, 1000);
},
Error,
"booom",
);
controller.abort();
});

Deno.test("deadline() handles non-aborted signal", async () => {
const controller = new AbortController();
const { signal } = controller;
const p = delay(100, { signal })
.catch(() => {})
.then(() => "Hello");
const abort = new AbortController();
const result = await deadline(p, 1000, { signal: abort.signal });
assertEquals(result, "Hello");
controller.abort();
});

Deno.test("deadline() handles aborted signal after delay", async () => {
const controller = new AbortController();
const { signal } = controller;
const p = delay(100, { signal })
.catch(() => {})
.then(() => "Hello");
const abort = new AbortController();
const promise = deadline(p, 100, { signal: abort.signal });
abort.abort();
const error = await assertRejects(
() => promise,
DOMException,
"The signal has been aborted",
);
assertEquals(error.name, "AbortError");
controller.abort();
});

Deno.test("deadline() handles already aborted signal", async () => {
const controller = new AbortController();
const { signal } = controller;
const p = delay(100, { signal })
.catch(() => {})
.then(() => "Hello");
const abort = new AbortController();
abort.abort();
const error = await assertRejects(
() => deadline(p, 100, { signal: abort.signal }),
DOMException,
"The signal has been aborted",
);
assertEquals(error.name, "AbortError");
controller.abort();
});

Deno.test("deadline() supports allowInfinity option", async () => {
await assertRejects(
() => deadline(Promise.resolve("Hello"), Infinity),
TypeError,
"Argument 1 is not a finite number",
);
await deadline(Promise.resolve("Hello"), Infinity, { allowInfinity: true });
});
Loading