Skip to content

Commit 8590446

Browse files
authored
Merge branch 'main' into tools-regexp-constant-case-name-check
2 parents e5812fe + 4221795 commit 8590446

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2633
-1396
lines changed

.github/labeler.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ async:
77
bytes:
88
- changed-files:
99
- any-glob-to-any-file: bytes/**
10+
cache:
11+
- changed-files:
12+
- any-glob-to-any-file: cache/**
1013
cbor:
1114
- changed-files:
1215
- any-glob-to-any-file: cbor/**
@@ -115,6 +118,9 @@ uuid:
115118
webgpu:
116119
- changed-files:
117120
- any-glob-to-any-file: webgpu/**
121+
xml:
122+
- changed-files:
123+
- any-glob-to-any-file: xml/**
118124
yaml:
119125
- changed-files:
120126
- any-glob-to-any-file: yaml/**

Releases.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,62 @@
1+
### 2026.02.20
2+
3+
#### @std/assert 1.0.19 (patch)
4+
5+
- fix(assert): preserve Date instances in assertObjectMatch filter (#6988)
6+
- docs(assert): add a note about unreliable equality (#6989)
7+
8+
#### @std/async 1.2.0 (minor)
9+
10+
- feat(async): stabilize abort signal support in retry (#6968)
11+
- feat(async/unstable): add `Lazy` for once-only async init (#7007)
12+
- feat(async/unstable): allow `AbortableOptions` with optional signal in
13+
`abortable` (#6971)
14+
- feat(async/unstable): add poll function (#6973)
15+
- feat(async/unstable): add allKeyed and allSettledKeyed (#6959)
16+
- refactor(async/unstable): apply style guide to circuit breaker (#7008)
17+
- refactor(async/unstable): make circuit breaker resilient to throwing callbacks
18+
(#6996)
19+
20+
#### @std/cli 1.0.28 (patch)
21+
22+
- feat(cli/unstable): introduce StaticLine (#6758)
23+
- fix(cli): handle empty value in parseArgs (#6995)
24+
- fix(cli): prevent prototype pollution in parseArgs (#6980)
25+
26+
#### @std/collections 1.1.6 (patch)
27+
28+
- fix(collections): stricter enforcement on generics (#6961)
29+
30+
#### @std/expect 1.0.18 (patch)
31+
32+
- feat(expect/unstable): implement toMatchSnapshot() (#7003)
33+
34+
#### @std/fs 1.0.23 (patch)
35+
36+
- fix(fs/unstable): use crypto.getRandomValues() for temp file naming (#6983)
37+
38+
#### @std/http 1.0.25 (patch)
39+
40+
- feat(http/unstable): add `Cache-Control` header parser and formatter (#7005)
41+
- feat(http/unstable): implement RFC 9651 Structured Field Values (#6963)
42+
- fix(http): guard top-level Deno global access for browser compatibility
43+
(#6987)
44+
- refactor(http/unstable): improve route() method matching (#6990)
45+
46+
#### @std/json 1.0.3 (patch)
47+
48+
- feat(json/unstable): implement RFC 8785 JSON canonicalization (#6965)
49+
50+
#### @std/xml 0.1.0 (minor)
51+
52+
- feat(xml/unstable): add XML parsing and serialization module (#6981)
53+
- docs(xml): clean up JSDoc and error message style (#7009)
54+
- refactor(xml): improve tokenizer edge cases and performance (#6997)
55+
56+
#### @std/yaml 1.0.12 (patch)
57+
58+
- refactor(yaml): add `Scanner` class (#6958)
59+
160
### 2026.01.30
261

362
#### @std/assert 1.0.18 (patch)

assert/deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@std/assert",
3-
"version": "1.0.18",
3+
"version": "1.0.19",
44
"exports": {
55
".": "./mod.ts",
66
"./assert": "./assert.ts",

async/deno.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@std/async",
3-
"version": "1.1.1",
3+
"version": "1.2.0",
44
"exports": {
55
".": "./mod.ts",
66
"./abortable": "./abortable.ts",
@@ -11,12 +11,13 @@
1111
"./unstable-mux-async-iterator": "./unstable_mux_async_iterator.ts",
1212
"./pool": "./pool.ts",
1313
"./retry": "./retry.ts",
14-
"./unstable-retry": "./unstable_retry.ts",
1514
"./tee": "./tee.ts",
15+
"./unstable-abortable": "./unstable_abortable.ts",
1616
"./unstable-throttle": "./unstable_throttle.ts",
1717
"./unstable-wait-for": "./unstable_wait_for.ts",
1818
"./unstable-semaphore": "./unstable_semaphore.ts",
1919
"./unstable-circuit-breaker": "./unstable_circuit_breaker.ts",
20+
"./unstable-lazy": "./unstable_lazy.ts",
2021
"./unstable-all-keyed": "./unstable_all_keyed.ts",
2122
"./unstable-poll": "./unstable_poll.ts"
2223
}

async/retry.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ export interface RetryOptions {
7070
* @returns `true` if the error is retriable, `false` otherwise.
7171
*/
7272
isRetriable?: (err: unknown) => boolean;
73+
/**
74+
* An AbortSignal to cancel the retry operation.
75+
*
76+
* If the signal is aborted, the retry will stop and reject with the signal's
77+
* reason. The signal is checked before each attempt and during the delay
78+
* between attempts.
79+
*
80+
* @default {undefined}
81+
*/
82+
signal?: AbortSignal;
7383
}
7484

7585
/**
@@ -149,6 +159,7 @@ export interface RetryOptions {
149159
* @param options Additional options.
150160
* @returns The promise that resolves with the value returned by the function to retry.
151161
* @throws {RetryError} If the function fails after `maxAttempts` attempts.
162+
* @throws If the `signal` is aborted, throws the signal's reason.
152163
* @throws If `isRetriable` returns `false` for an error, throws that error immediately.
153164
*/
154165
export async function retry<T>(
@@ -162,6 +173,7 @@ export async function retry<T>(
162173
minTimeout = 1000,
163174
jitter = 1,
164175
isRetriable = () => true,
176+
signal,
165177
} = options ?? {};
166178

167179
if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
@@ -197,6 +209,8 @@ export async function retry<T>(
197209

198210
let attempt = 0;
199211
while (true) {
212+
signal?.throwIfAborted();
213+
200214
try {
201215
return await fn();
202216
} catch (error) {
@@ -215,7 +229,7 @@ export async function retry<T>(
215229
multiplier,
216230
jitter,
217231
);
218-
await delay(timeout);
232+
await delay(timeout, signal ? { signal } : undefined);
219233
}
220234
attempt++;
221235
}

async/retry_test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,3 +379,51 @@ Deno.test("retry() only retries errors that are retriable with `isRetriable` opt
379379
}, options), HttpError);
380380
assertEquals(numCalls, 3);
381381
});
382+
383+
Deno.test("retry() aborts during delay when signal is aborted", async () => {
384+
using time = new FakeTime();
385+
const controller = new AbortController();
386+
let attempts = 0;
387+
388+
const promise = retry(() => {
389+
attempts++;
390+
throw new Error("fail");
391+
}, { signal: controller.signal, jitter: 0 });
392+
393+
await time.nextAsync(); // First delay starts (1000ms)
394+
controller.abort("cancelled");
395+
396+
const error = await assertRejects(() => promise);
397+
assertEquals(error, "cancelled");
398+
assertEquals(attempts, 2); // Only 2 attempts, not 5
399+
});
400+
401+
Deno.test("retry() throws immediately if signal is already aborted", async () => {
402+
const controller = new AbortController();
403+
controller.abort("pre-aborted");
404+
let called = false;
405+
406+
const error = await assertRejects(
407+
() =>
408+
retry(() => {
409+
called = true;
410+
return "ok";
411+
}, { signal: controller.signal }),
412+
);
413+
assertEquals(error, "pre-aborted");
414+
assertEquals(called, false); // fn was never called
415+
});
416+
417+
Deno.test("retry() throws AbortError when signal is aborted without reason", async () => {
418+
const controller = new AbortController();
419+
controller.abort(); // No reason = DOMException with name "AbortError"
420+
421+
const error = await assertRejects(
422+
() =>
423+
retry(() => {
424+
throw new Error();
425+
}, { signal: controller.signal }),
426+
DOMException,
427+
);
428+
assertEquals(error.name, "AbortError");
429+
});

async/unstable_abortable.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
// This module is browser compatible.
3+
4+
/** Options for {@linkcode abortable}. */
5+
export interface AbortableOptions {
6+
/** The signal to abort the promise with. */
7+
signal?: AbortSignal | undefined;
8+
}
9+
10+
// TODO(iuioiua): Remove `ignore` directives from following snippets
11+
/**
12+
* Make a {@linkcode Promise} abortable with the given signal.
13+
*
14+
* @throws {DOMException} If the signal is already aborted and `signal.reason`
15+
* is undefined. Otherwise, throws `signal.reason`.
16+
* @typeParam T The type of the provided and returned promise.
17+
* @param p The promise to make abortable.
18+
* @param signal The signal to abort the promise with.
19+
* @returns A promise that can be aborted.
20+
*
21+
* @example Error-handling a timeout
22+
* ```ts ignore
23+
* import { abortable, delay } from "@std/async";
24+
* import { assertRejects, assertEquals } from "@std/assert";
25+
*
26+
* const promise = delay(1_000);
27+
*
28+
* // Rejects with `DOMException` after 100 ms
29+
* await assertRejects(
30+
* () => abortable(promise, AbortSignal.timeout(100)),
31+
* DOMException,
32+
* "Signal timed out."
33+
* );
34+
* ```
35+
*
36+
* @example Error-handling an abort
37+
* ```ts ignore
38+
* import { abortable, delay } from "@std/async";
39+
* import { assertRejects, assertEquals } from "@std/assert";
40+
*
41+
* const promise = delay(1_000);
42+
* const controller = new AbortController();
43+
* controller.abort(new Error("This is my reason"));
44+
*
45+
* // Rejects with `DOMException` immediately
46+
* await assertRejects(
47+
* () => abortable(promise, controller.signal),
48+
* Error,
49+
* "This is my reason"
50+
* );
51+
* ```
52+
*/
53+
export function abortable<T>(
54+
p: Promise<T>,
55+
signal: AbortSignal | AbortableOptions,
56+
): Promise<T>;
57+
/**
58+
* Make an {@linkcode AsyncIterable} abortable with the given signal.
59+
*
60+
* @throws {DOMException} If the signal is already aborted and `signal.reason`
61+
* is undefined. Otherwise, throws `signal.reason`.
62+
* @typeParam T The type of the provided and returned async iterable.
63+
* @param p The async iterable to make abortable.
64+
* @param signal The signal to abort the promise with.
65+
* @returns An async iterable that can be aborted.
66+
*
67+
* @example Error-handling a timeout
68+
* ```ts
69+
* import { abortable, delay } from "@std/async";
70+
* import { assertRejects, assertEquals } from "@std/assert";
71+
*
72+
* const asyncIter = async function* () {
73+
* yield "Hello";
74+
* await delay(1_000);
75+
* yield "World";
76+
* };
77+
*
78+
* const items: string[] = [];
79+
* // Below throws `DOMException` after 100 ms and items become `["Hello"]`
80+
* await assertRejects(
81+
* async () => {
82+
* for await (const item of abortable(asyncIter(), AbortSignal.timeout(100))) {
83+
* items.push(item);
84+
* }
85+
* },
86+
* DOMException,
87+
* "Signal timed out."
88+
* );
89+
* assertEquals(items, ["Hello"]);
90+
* ```
91+
*
92+
* @example Error-handling an abort
93+
* ```ts
94+
* import { abortable, delay } from "@std/async";
95+
* import { assertRejects, assertEquals } from "@std/assert";
96+
*
97+
* const asyncIter = async function* () {
98+
* yield "Hello";
99+
* await delay(1_000);
100+
* yield "World";
101+
* };
102+
* const controller = new AbortController();
103+
* controller.abort(new Error("This is my reason"));
104+
*
105+
* const items: string[] = [];
106+
* // Below throws `DOMException` immediately
107+
* await assertRejects(
108+
* async () => {
109+
* for await (const item of abortable(asyncIter(), controller.signal)) {
110+
* items.push(item);
111+
* }
112+
* },
113+
* Error,
114+
* "This is my reason"
115+
* );
116+
* assertEquals(items, []);
117+
* ```
118+
*/
119+
120+
export function abortable<T>(
121+
p: AsyncIterable<T>,
122+
signal: AbortSignal | AbortableOptions,
123+
): AsyncGenerator<T>;
124+
export function abortable<T>(
125+
p: Promise<T> | AsyncIterable<T>,
126+
signal: AbortSignal | AbortableOptions,
127+
): Promise<T> | AsyncIterable<T> {
128+
if (!(signal instanceof AbortSignal)) {
129+
if (!signal.signal) {
130+
return p;
131+
}
132+
signal = signal.signal;
133+
}
134+
if (p instanceof Promise) {
135+
return abortablePromise(p, signal);
136+
} else {
137+
return abortableAsyncIterable(p, signal);
138+
}
139+
}
140+
141+
function abortablePromise<T>(
142+
p: Promise<T>,
143+
signal: AbortSignal,
144+
): Promise<T> {
145+
const { promise, reject } = Promise.withResolvers<never>();
146+
const abort = () => reject(signal.reason);
147+
if (signal.aborted) abort();
148+
signal.addEventListener("abort", abort, { once: true });
149+
return Promise.race([promise, p]).finally(() => {
150+
signal.removeEventListener("abort", abort);
151+
});
152+
}
153+
154+
async function* abortableAsyncIterable<T>(
155+
p: AsyncIterable<T>,
156+
signal: AbortSignal,
157+
): AsyncGenerator<T> {
158+
signal.throwIfAborted();
159+
const { promise, reject } = Promise.withResolvers<never>();
160+
const abort = () => reject(signal.reason);
161+
signal.addEventListener("abort", abort, { once: true });
162+
163+
const it = p[Symbol.asyncIterator]();
164+
try {
165+
while (true) {
166+
const race = Promise.race([promise, it.next()]);
167+
race.catch(() => {
168+
signal.removeEventListener("abort", abort);
169+
});
170+
const { done, value } = await race;
171+
if (done) {
172+
signal.removeEventListener("abort", abort);
173+
const result = await it.return?.(value);
174+
return result?.value;
175+
}
176+
yield value;
177+
}
178+
} catch (e) {
179+
await it.return?.();
180+
throw e;
181+
}
182+
}

0 commit comments

Comments
 (0)