Skip to content

Commit e561d36

Browse files
authored
BREAKING(async): simplify deadline() logic, remove DeadlineError and improve errors (denoland#5058)
1 parent 32d46e9 commit e561d36

File tree

8 files changed

+163
-132
lines changed

8 files changed

+163
-132
lines changed

async/_util.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
22
// This module is browser compatible.
33

4-
// This `reason` comes from `AbortSignal` thus must be `any`.
5-
// deno-lint-ignore no-explicit-any
6-
export function createAbortError(reason?: any): DOMException {
7-
return new DOMException(
8-
reason ? `Aborted: ${reason}` : "Aborted",
9-
"AbortError",
10-
);
11-
}
12-
134
export function exponentialBackoffWithJitter(
145
cap: number,
156
base: number,

async/_util_test.ts

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2-
import { createAbortError, exponentialBackoffWithJitter } from "./_util.ts";
3-
import { assertEquals, assertInstanceOf } from "@std/assert";
2+
import { exponentialBackoffWithJitter } from "./_util.ts";
3+
import { assertEquals } from "@std/assert";
44

55
// test util to ensure deterministic results during testing of backoff function by polyfilling Math.random
66
function prngMulberry32(seed: number) {
@@ -50,31 +50,3 @@ Deno.test("exponentialBackoffWithJitter()", () => {
5050
assertEquals(results as typeof row, row);
5151
}
5252
});
53-
54-
Deno.test("createAbortError()", () => {
55-
const error = createAbortError();
56-
assertInstanceOf(error, DOMException);
57-
assertEquals(error.name, "AbortError");
58-
assertEquals(error.message, "Aborted");
59-
});
60-
61-
Deno.test("createAbortError() handles aborted signal with reason", () => {
62-
const c = new AbortController();
63-
c.abort("Expected Reason");
64-
const error = createAbortError(c.signal.reason);
65-
assertInstanceOf(error, DOMException);
66-
assertEquals(error.name, "AbortError");
67-
assertEquals(error.message, "Aborted: Expected Reason");
68-
});
69-
70-
Deno.test("createAbortError() handles aborted signal without reason", () => {
71-
const c = new AbortController();
72-
c.abort();
73-
const error = createAbortError(c.signal.reason);
74-
assertInstanceOf(error, DOMException);
75-
assertEquals(error.name, "AbortError");
76-
assertEquals(
77-
error.message,
78-
"Aborted: AbortError: The signal has been aborted",
79-
);
80-
});

async/abortable.ts

Lines changed: 80 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,109 @@
11
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
22
// This module is browser compatible.
33

4-
import { createAbortError } from "./_util.ts";
5-
64
/**
75
* Make a {@linkcode Promise} abortable with the given signal.
86
*
7+
* @throws {DOMException} If the signal is already aborted and `signal.reason`
8+
* is undefined. Otherwise, throws `signal.reason`.
99
* @typeParam T The type of the provided and returned promise.
1010
* @param p The promise to make abortable.
1111
* @param signal The signal to abort the promise with.
1212
* @returns A promise that can be aborted.
1313
*
14-
* @example Usage
15-
* ```ts no-eval
16-
* import {
17-
* abortable,
18-
* delay,
19-
* } from "@std/async";
14+
* @example Error-handling a timeout
15+
* ```ts
16+
* import { abortable, delay } from "@std/async";
17+
* import { assertRejects, assertEquals } from "@std/assert";
18+
*
19+
* const promise = delay(1_000);
20+
*
21+
* // Rejects with `DOMException` after 100 ms
22+
* await assertRejects(
23+
* () => abortable(promise, AbortSignal.timeout(100)),
24+
* DOMException,
25+
* "Signal timed out."
26+
* );
27+
* ```
28+
*
29+
* @example Error-handling an abort
30+
* ```ts
31+
* import { abortable, delay } from "@std/async";
32+
* import { assertRejects, assertEquals } from "@std/assert";
2033
*
21-
* const p = delay(1000);
22-
* const c = new AbortController();
23-
* setTimeout(() => c.abort(), 100);
34+
* const promise = delay(1_000);
35+
* const controller = new AbortController();
36+
* controller.abort(new Error("This is my reason"));
2437
*
25-
* // Below throws `DOMException` after 100 ms
26-
* await abortable(p, c.signal);
38+
* // Rejects with `DOMException` immediately
39+
* await assertRejects(
40+
* () => abortable(promise, controller.signal),
41+
* Error,
42+
* "This is my reason"
43+
* );
2744
* ```
2845
*/
2946
export function abortable<T>(p: Promise<T>, signal: AbortSignal): Promise<T>;
3047
/**
3148
* Make an {@linkcode AsyncIterable} abortable with the given signal.
3249
*
50+
* @throws {DOMException} If the signal is already aborted and `signal.reason`
51+
* is undefined. Otherwise, throws `signal.reason`.
3352
* @typeParam T The type of the provided and returned async iterable.
3453
* @param p The async iterable to make abortable.
3554
* @param signal The signal to abort the promise with.
3655
* @returns An async iterable that can be aborted.
3756
*
38-
* @example Usage
39-
* ```ts no-eval
40-
* import {
41-
* abortable,
42-
* delay,
43-
* } from "@std/async";
57+
* @example Error-handling a timeout
58+
* ```ts
59+
* import { abortable, delay } from "@std/async";
60+
* import { assertRejects, assertEquals } from "@std/assert";
4461
*
45-
* const p = async function* () {
62+
* const asyncIter = async function* () {
4663
* yield "Hello";
47-
* await delay(1000);
64+
* await delay(1_000);
4865
* yield "World";
4966
* };
50-
* const c = new AbortController();
51-
* setTimeout(() => c.abort(), 100);
5267
*
53-
* // Below throws `DOMException` after 100 ms
54-
* // and items become `["Hello"]`
5568
* const items: string[] = [];
56-
* for await (const item of abortable(p(), c.signal)) {
57-
* items.push(item);
58-
* }
69+
* // Below throws `DOMException` after 100 ms and items become `["Hello"]`
70+
* await assertRejects(
71+
* async () => {
72+
* for await (const item of abortable(asyncIter(), AbortSignal.timeout(100))) {
73+
* items.push(item);
74+
* }
75+
* },
76+
* DOMException,
77+
* "Signal timed out."
78+
* );
79+
* assertEquals(items, ["Hello"]);
80+
* ```
81+
*
82+
* @example Error-handling an abort
83+
* ```ts
84+
* import { abortable, delay } from "@std/async";
85+
* import { assertRejects, assertEquals } from "@std/assert";
86+
*
87+
* const asyncIter = async function* () {
88+
* yield "Hello";
89+
* await delay(1_000);
90+
* yield "World";
91+
* };
92+
* const controller = new AbortController();
93+
* controller.abort(new Error("This is my reason"));
94+
*
95+
* const items: string[] = [];
96+
* // Below throws `DOMException` immediately
97+
* await assertRejects(
98+
* async () => {
99+
* for await (const item of abortable(asyncIter(), controller.signal)) {
100+
* items.push(item);
101+
* }
102+
* },
103+
* Error,
104+
* "This is my reason"
105+
* );
106+
* assertEquals(items, []);
59107
* ```
60108
*/
61109
export function abortable<T>(
@@ -77,11 +125,9 @@ function abortablePromise<T>(
77125
p: Promise<T>,
78126
signal: AbortSignal,
79127
): Promise<T> {
80-
if (signal.aborted) {
81-
return Promise.reject(createAbortError(signal.reason));
82-
}
128+
if (signal.aborted) return Promise.reject(signal.reason);
83129
const { promise, reject } = Promise.withResolvers<never>();
84-
const abort = () => reject(createAbortError(signal.reason));
130+
const abort = () => reject(signal.reason);
85131
signal.addEventListener("abort", abort, { once: true });
86132
return Promise.race([promise, p]).finally(() => {
87133
signal.removeEventListener("abort", abort);
@@ -92,11 +138,9 @@ async function* abortableAsyncIterable<T>(
92138
p: AsyncIterable<T>,
93139
signal: AbortSignal,
94140
): AsyncGenerator<T> {
95-
if (signal.aborted) {
96-
throw createAbortError(signal.reason);
97-
}
141+
signal.throwIfAborted();
98142
const { promise, reject } = Promise.withResolvers<never>();
99-
const abort = () => reject(createAbortError(signal.reason));
143+
const abort = () => reject(signal.reason);
100144
signal.addEventListener("abort", abort, { once: true });
101145

102146
const it = p[Symbol.asyncIterator]();

async/abortable_test.ts

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,24 @@ Deno.test("abortable() handles promise with aborted signal after delay", async (
1616
const { promise, resolve } = Promise.withResolvers<string>();
1717
const t = setTimeout(() => resolve("Hello"), 100);
1818
setTimeout(() => c.abort(), 50);
19-
await assertRejects(
20-
async () => {
21-
await abortable(promise, c.signal);
22-
},
19+
const error = await assertRejects(
20+
() => abortable(promise, c.signal),
2321
DOMException,
24-
"AbortError",
22+
"The signal has been aborted",
23+
);
24+
assertEquals(error.name, "AbortError");
25+
clearTimeout(t);
26+
});
27+
28+
Deno.test("abortable() handles promise with aborted signal after delay with reason", async () => {
29+
const c = new AbortController();
30+
const { promise, resolve } = Promise.withResolvers<string>();
31+
const t = setTimeout(() => resolve("Hello"), 100);
32+
setTimeout(() => c.abort(new Error("This is my reason")), 50);
33+
await assertRejects(
34+
() => abortable(promise, c.signal),
35+
Error,
36+
"This is my reason",
2537
);
2638
clearTimeout(t);
2739
});
@@ -31,12 +43,26 @@ Deno.test("abortable() handles promise with already aborted signal", async () =>
3143
const { promise, resolve } = Promise.withResolvers<string>();
3244
const t = setTimeout(() => resolve("Hello"), 100);
3345
c.abort();
34-
await assertRejects(
46+
const error = await assertRejects(
3547
async () => {
3648
await abortable(promise, c.signal);
3749
},
3850
DOMException,
39-
"AbortError",
51+
"The signal has been aborted",
52+
);
53+
assertEquals(error.name, "AbortError");
54+
clearTimeout(t);
55+
});
56+
57+
Deno.test("abortable() handles promise with already aborted signal with reason", async () => {
58+
const c = new AbortController();
59+
const { promise, resolve } = Promise.withResolvers<string>();
60+
const t = setTimeout(() => resolve("Hello"), 100);
61+
c.abort(new Error("This is my reason"));
62+
await assertRejects(
63+
() => abortable(promise, c.signal),
64+
Error,
65+
"This is my reason",
4066
);
4167
clearTimeout(t);
4268
});
@@ -66,15 +92,16 @@ Deno.test("abortable.AsyncIterable() handles aborted signal after delay", async
6692
};
6793
setTimeout(() => c.abort(), 50);
6894
const items: string[] = [];
69-
await assertRejects(
95+
const error = await assertRejects(
7096
async () => {
7197
for await (const item of abortable(a(), c.signal)) {
7298
items.push(item);
7399
}
74100
},
75101
DOMException,
76-
"AbortError",
102+
"The signal has been aborted",
77103
);
104+
assertEquals(error.name, "AbortError");
78105
assertEquals(items, ["Hello"]);
79106
clearTimeout(t);
80107
});
@@ -90,15 +117,16 @@ Deno.test("abortable.AsyncIterable() handles already aborted signal", async () =
90117
};
91118
c.abort();
92119
const items: string[] = [];
93-
await assertRejects(
120+
const error = await assertRejects(
94121
async () => {
95122
for await (const item of abortable(a(), c.signal)) {
96123
items.push(item);
97124
}
98125
},
99126
DOMException,
100-
"AbortError",
127+
"The signal has been aborted",
101128
);
129+
assertEquals(error.name, "AbortError");
102130
assertEquals(items, []);
103131
clearTimeout(t);
104132
});

async/deadline.ts

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2-
// This module is browser compatible.
3-
4-
import { delay } from "./delay.ts";
2+
// TODO(iuioiua): Add web-compatible declaration once TypeScript 5.5 is released
3+
// and in the Deno runtime. See https://github.com/microsoft/TypeScript/pull/58211
4+
//
5+
// Note: this code is still compatible with recent
6+
// web browsers. See https://caniuse.com/?search=AbortSignal.any
7+
import { abortable } from "./abortable.ts";
58

69
/** Options for {@linkcode deadline}. */
710
export interface DeadlineOptions {
@@ -10,29 +13,14 @@ export interface DeadlineOptions {
1013
}
1114

1215
/**
13-
* Error thrown when {@linkcode deadline} times out.
14-
*
15-
* @example Usage
16-
* ```ts no-assert
17-
* import { DeadlineError } from "@std/async/deadline";
18-
*
19-
* const error = new DeadlineError();
20-
* ```
21-
*/
22-
export class DeadlineError extends Error {
23-
constructor() {
24-
super("Deadline");
25-
this.name = this.constructor.name;
26-
}
27-
}
28-
29-
/**
30-
* Create a promise which will be rejected with {@linkcode DeadlineError} when
16+
* Create a promise which will be rejected with {@linkcode DOMException} when
3117
* a given delay is exceeded.
3218
*
3319
* Note: Prefer to use {@linkcode AbortSignal.timeout} instead for the APIs
3420
* that accept {@linkcode AbortSignal}.
3521
*
22+
* @throws {DOMException} When the provided duration runs out before resolving
23+
* or if the optional signal is aborted, and `signal.reason` is undefined.
3624
* @typeParam T The type of the provided and returned promise.
3725
* @param p The promise to make rejectable.
3826
* @param ms Duration in milliseconds for when the promise should time out.
@@ -44,24 +32,17 @@ export class DeadlineError extends Error {
4432
* import { deadline } from "@std/async/deadline";
4533
* import { delay } from "@std/async/delay";
4634
*
47-
* const delayedPromise = delay(1000);
48-
* // Below throws `DeadlineError` after 10 ms
35+
* const delayedPromise = delay(1_000);
36+
* // Below throws `DOMException` after 10 ms
4937
* const result = await deadline(delayedPromise, 10);
5038
* ```
5139
*/
52-
export function deadline<T>(
40+
export async function deadline<T>(
5341
p: Promise<T>,
5442
ms: number,
5543
options: DeadlineOptions = {},
5644
): Promise<T> {
57-
const controller = new AbortController();
58-
const { signal } = options;
59-
if (signal?.aborted) {
60-
return Promise.reject(new DeadlineError());
61-
}
62-
signal?.addEventListener("abort", () => controller.abort(signal.reason));
63-
const d = delay(ms, { signal: controller.signal })
64-
.catch(() => {}) // Do NOTHING on abort.
65-
.then(() => Promise.reject(new DeadlineError()));
66-
return Promise.race([p.finally(() => controller.abort()), d]);
45+
const signals = [AbortSignal.timeout(ms)];
46+
if (options.signal) signals.push(options.signal);
47+
return await abortable(p, AbortSignal.any(signals));
6748
}

0 commit comments

Comments
 (0)