Skip to content

Commit 0bf1cff

Browse files
feat(async/unstable): add allKeyed and allSettledKeyed
1 parent 0aa59e2 commit 0bf1cff

File tree

3 files changed

+384
-2
lines changed

3 files changed

+384
-2
lines changed

async/deno.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"./unstable-throttle": "./unstable_throttle.ts",
1717
"./unstable-wait-for": "./unstable_wait_for.ts",
1818
"./unstable-semaphore": "./unstable_semaphore.ts",
19-
"./unstable-circuit-breaker": "./unstable_circuit_breaker.ts"
19+
"./unstable-circuit-breaker": "./unstable_circuit_breaker.ts",
20+
"./unstable-all-keyed": "./unstable_all_keyed.ts"
2021
}
21-
}
22+
}

async/unstable_all_keyed.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
// This module is browser compatible.
3+
4+
// TC39 spec uses [[OwnPropertyKeys]] (Reflect.ownKeys) then filters for enumerable.
5+
// This equivalent is faster on V8: Object.keys returns only enumerable string keys,
6+
// so we only need to filter symbol keys for enumerability. See also assert/equal.ts.
7+
function getEnumerableKeys(obj: object): PropertyKey[] {
8+
const stringKeys = Object.keys(obj);
9+
const symbolKeys = Object.getOwnPropertySymbols(obj).filter((key) =>
10+
Object.prototype.propertyIsEnumerable.call(obj, key)
11+
);
12+
return [...stringKeys, ...symbolKeys];
13+
}
14+
15+
/**
16+
* A record type where values can be promise-like (thenables) or plain values.
17+
*
18+
* @typeParam T The base record type with resolved value types.
19+
*/
20+
export type PromiseRecord<T extends Record<PropertyKey, unknown>> = {
21+
[K in keyof T]: PromiseLike<T[K]> | T[K];
22+
};
23+
24+
/**
25+
* A record type where values are {@linkcode PromiseSettledResult} objects.
26+
*
27+
* @typeParam T The base record type with resolved value types.
28+
*/
29+
export type SettledRecord<T extends Record<PropertyKey, unknown>> = {
30+
[K in keyof T]: PromiseSettledResult<T[K]>;
31+
};
32+
33+
/**
34+
* Resolves all values in a record of promises in parallel, returning a promise
35+
* that resolves to a record with the same keys and resolved values.
36+
*
37+
* This is similar to {@linkcode Promise.all}, but for records instead of arrays,
38+
* allowing you to use named keys instead of positional indices.
39+
*
40+
* If any promise rejects, the returned promise immediately rejects with the
41+
* first rejection reason. The result object has a null prototype, matching the
42+
* TC39 specification.
43+
*
44+
* This function implements the behavior proposed in the TC39
45+
* {@link https://github.com/tc39/proposal-await-dictionary | Await Dictionary}
46+
* proposal (`Promise.allKeyed`).
47+
*
48+
* @experimental **UNSTABLE**: New API, yet to be vetted.
49+
*
50+
* @typeParam T The record shape with resolved (unwrapped) value types. For
51+
* example, if passing `{ foo: Promise<number> }`, `T` would be `{ foo: number }`.
52+
* @param record A record where values are promise-like (thenables) or plain values.
53+
* @returns A promise that resolves to a record with the same keys and resolved
54+
* values. The result has a null prototype.
55+
* @throws Rejects with the first rejection reason if any promise in the record
56+
* rejects.
57+
*
58+
* @example Basic usage
59+
* ```ts
60+
* import { allKeyed } from "@std/async/unstable-all-keyed";
61+
* import { assertEquals } from "@std/assert";
62+
*
63+
* const result = await allKeyed({
64+
* foo: Promise.resolve(1),
65+
* bar: Promise.resolve("hello"),
66+
* });
67+
*
68+
* assertEquals(result, { foo: 1, bar: "hello" });
69+
* ```
70+
*
71+
* @example Parallel HTTP requests
72+
* ```ts no-assert ignore
73+
* import { allKeyed } from "@std/async/unstable-all-keyed";
74+
*
75+
* const { user, posts } = await allKeyed({
76+
* user: fetch("/api/user").then((r) => r.json()),
77+
* posts: fetch("/api/posts").then((r) => r.json()),
78+
* });
79+
* ```
80+
*
81+
* @example Mixed promises and plain values
82+
* ```ts
83+
* import { allKeyed } from "@std/async/unstable-all-keyed";
84+
* import { assertEquals } from "@std/assert";
85+
*
86+
* const result = await allKeyed({
87+
* promised: Promise.resolve(42),
88+
* plain: "static",
89+
* });
90+
*
91+
* assertEquals(result, { promised: 42, plain: "static" });
92+
* ```
93+
*/
94+
export function allKeyed<T extends Record<PropertyKey, unknown>>(
95+
record: PromiseRecord<T>,
96+
): Promise<T> {
97+
const keys = getEnumerableKeys(record);
98+
const values = keys.map((key) => record[key as keyof typeof record]);
99+
100+
return Promise.all(values).then((resolved) => {
101+
const result = Object.create(null) as T;
102+
for (let i = 0; i < keys.length; i++) {
103+
result[keys[i] as keyof T] = resolved[i] as T[keyof T];
104+
}
105+
return result;
106+
});
107+
}
108+
109+
/**
110+
* Resolves all values in a record of promises in parallel, returning a promise
111+
* that resolves to a record with the same keys and {@linkcode PromiseSettledResult}
112+
* objects as values.
113+
*
114+
* This is similar to {@linkcode Promise.allSettled}, but for records instead of
115+
* arrays, allowing you to use named keys instead of positional indices.
116+
*
117+
* Unlike {@linkcode allKeyed}, this function never rejects due to promise
118+
* rejections. Instead, each value in the result record is a
119+
* {@linkcode PromiseSettledResult} object indicating whether the corresponding
120+
* promise was fulfilled or rejected. The result object has a null prototype,
121+
* matching the TC39 specification.
122+
*
123+
* This function implements the behavior proposed in the TC39
124+
* {@link https://github.com/tc39/proposal-await-dictionary | Await Dictionary}
125+
* proposal (`Promise.allSettledKeyed`).
126+
*
127+
* @experimental **UNSTABLE**: New API, yet to be vetted.
128+
*
129+
* @typeParam T The record shape with resolved (unwrapped) value types. For
130+
* example, if passing `{ foo: Promise<number> }`, `T` would be `{ foo: number }`.
131+
* @param record A record where values are promise-like (thenables) or plain values.
132+
* @returns A promise that resolves to a record with the same keys and
133+
* {@linkcode PromiseSettledResult} values. The result has a null prototype.
134+
*
135+
* @example Basic usage
136+
* ```ts
137+
* import { allSettledKeyed } from "@std/async/unstable-all-keyed";
138+
* import { assertEquals } from "@std/assert";
139+
*
140+
* const settled = await allSettledKeyed({
141+
* success: Promise.resolve(1),
142+
* failure: Promise.reject(new Error("oops")),
143+
* });
144+
*
145+
* assertEquals(settled.success, { status: "fulfilled", value: 1 });
146+
* assertEquals(settled.failure.status, "rejected");
147+
* ```
148+
*
149+
* @example Error handling
150+
* ```ts
151+
* import { allSettledKeyed } from "@std/async/unstable-all-keyed";
152+
* import { assertEquals, assertExists } from "@std/assert";
153+
*
154+
* const settled = await allSettledKeyed({
155+
* a: Promise.resolve("ok"),
156+
* b: Promise.reject(new Error("fail")),
157+
* c: Promise.resolve("also ok"),
158+
* });
159+
*
160+
* // Check individual results
161+
* if (settled.a.status === "fulfilled") {
162+
* assertEquals(settled.a.value, "ok");
163+
* }
164+
* if (settled.b.status === "rejected") {
165+
* assertExists(settled.b.reason);
166+
* }
167+
* ```
168+
*/
169+
export function allSettledKeyed<T extends Record<PropertyKey, unknown>>(
170+
record: PromiseRecord<T>,
171+
): Promise<SettledRecord<T>> {
172+
const keys = getEnumerableKeys(record);
173+
const values = keys.map((key) => record[key as keyof typeof record]);
174+
175+
return Promise.allSettled(values).then((settled) => {
176+
const result = Object.create(null) as SettledRecord<T>;
177+
for (let i = 0; i < keys.length; i++) {
178+
result[keys[i] as keyof T] = settled[i] as PromiseSettledResult<
179+
T[keyof T]
180+
>;
181+
}
182+
return result;
183+
});
184+
}

async/unstable_all_keyed_test.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
import { allKeyed, allSettledKeyed } from "./unstable_all_keyed.ts";
3+
import {
4+
assertEquals,
5+
assertFalse,
6+
assertRejects,
7+
assertStrictEquals,
8+
} from "@std/assert";
9+
10+
// allKeyed tests
11+
12+
Deno.test("allKeyed() resolves record of promises", async () => {
13+
const result = await allKeyed({
14+
a: Promise.resolve(1),
15+
b: Promise.resolve("two"),
16+
c: Promise.resolve(true),
17+
});
18+
19+
assertEquals(result, { a: 1, b: "two", c: true });
20+
});
21+
22+
Deno.test("allKeyed() handles mixed promises and plain values", async () => {
23+
const result = await allKeyed({
24+
promise: Promise.resolve(42),
25+
plain: "static",
26+
anotherPromise: Promise.resolve([1, 2, 3]),
27+
});
28+
29+
assertEquals(result, {
30+
promise: 42,
31+
plain: "static",
32+
anotherPromise: [1, 2, 3],
33+
});
34+
});
35+
36+
Deno.test("allKeyed() handles empty record", async () => {
37+
const result = await allKeyed({});
38+
assertEquals(result, {});
39+
});
40+
41+
Deno.test("allKeyed() rejects on first rejection", async () => {
42+
const error = new Error("test error");
43+
44+
await assertRejects(
45+
() =>
46+
allKeyed({
47+
a: Promise.resolve(1),
48+
b: Promise.reject(error),
49+
c: Promise.resolve(3),
50+
}),
51+
Error,
52+
"test error",
53+
);
54+
});
55+
56+
Deno.test("allKeyed() preserves symbol keys", async () => {
57+
const sym = Symbol("test");
58+
const result = await allKeyed({
59+
[sym]: Promise.resolve("symbol value"),
60+
regular: Promise.resolve("regular value"),
61+
});
62+
63+
assertEquals(result[sym], "symbol value");
64+
assertEquals(result.regular, "regular value");
65+
});
66+
67+
Deno.test("allKeyed() ignores non-enumerable properties", async () => {
68+
const record = Object.create(null);
69+
Object.defineProperty(record, "enumerable", {
70+
value: Promise.resolve("visible"),
71+
enumerable: true,
72+
});
73+
Object.defineProperty(record, "nonEnumerable", {
74+
value: Promise.resolve("hidden"),
75+
enumerable: false,
76+
});
77+
78+
const result = await allKeyed(record);
79+
80+
assertEquals(result, { enumerable: "visible" });
81+
assertEquals(Object.keys(result), ["enumerable"]);
82+
});
83+
84+
// allSettledKeyed tests
85+
86+
Deno.test("allSettledKeyed() resolves all promises", async () => {
87+
const result = await allSettledKeyed({
88+
a: Promise.resolve(1),
89+
b: Promise.resolve("two"),
90+
});
91+
92+
assertEquals(result, {
93+
a: { status: "fulfilled", value: 1 },
94+
b: { status: "fulfilled", value: "two" },
95+
});
96+
});
97+
98+
Deno.test("allSettledKeyed() handles mixed fulfilled and rejected", async () => {
99+
const error = new Error("rejection reason");
100+
const result = await allSettledKeyed({
101+
success: Promise.resolve("ok"),
102+
failure: Promise.reject(error),
103+
});
104+
105+
assertEquals(result.success, { status: "fulfilled", value: "ok" });
106+
assertEquals(result.failure.status, "rejected");
107+
assertEquals((result.failure as PromiseRejectedResult).reason, error);
108+
});
109+
110+
Deno.test("allSettledKeyed() handles all rejections without throwing", async () => {
111+
const error1 = new Error("error 1");
112+
const error2 = new Error("error 2");
113+
114+
const result = await allSettledKeyed({
115+
a: Promise.reject(error1),
116+
b: Promise.reject(error2),
117+
});
118+
119+
assertEquals(result.a.status, "rejected");
120+
assertEquals(result.b.status, "rejected");
121+
assertEquals((result.a as PromiseRejectedResult).reason, error1);
122+
assertEquals((result.b as PromiseRejectedResult).reason, error2);
123+
});
124+
125+
Deno.test("allSettledKeyed() handles empty record", async () => {
126+
const result = await allSettledKeyed({});
127+
assertEquals(result, {});
128+
});
129+
130+
Deno.test("allSettledKeyed() preserves symbol keys", async () => {
131+
const sym = Symbol("test");
132+
const result = await allSettledKeyed({
133+
[sym]: Promise.resolve("symbol"),
134+
regular: Promise.reject(new Error("fail")),
135+
});
136+
137+
assertEquals(result[sym], { status: "fulfilled", value: "symbol" });
138+
assertEquals(result.regular.status, "rejected");
139+
});
140+
141+
Deno.test("allKeyed() returns object with null prototype", async () => {
142+
const result = await allKeyed({ a: Promise.resolve(1) });
143+
144+
assertStrictEquals(Object.getPrototypeOf(result), null);
145+
assertFalse("hasOwnProperty" in result);
146+
assertFalse("toString" in result);
147+
});
148+
149+
Deno.test("allSettledKeyed() returns object with null prototype", async () => {
150+
const result = await allSettledKeyed({ a: Promise.resolve(1) });
151+
152+
assertStrictEquals(Object.getPrototypeOf(result), null);
153+
assertFalse("hasOwnProperty" in result);
154+
assertFalse("toString" in result);
155+
});
156+
157+
Deno.test("allKeyed() preserves numeric key order", async () => {
158+
const result = await allKeyed({
159+
"2": Promise.resolve("two"),
160+
"1": Promise.resolve("one"),
161+
"10": Promise.resolve("ten"),
162+
});
163+
164+
assertEquals(Object.keys(result), ["1", "2", "10"]);
165+
assertEquals(result["1"], "one");
166+
assertEquals(result["2"], "two");
167+
assertEquals(result["10"], "ten");
168+
});
169+
170+
Deno.test("allKeyed() ignores inherited properties", async () => {
171+
const proto = { inherited: Promise.resolve("from proto") };
172+
const record = Object.create(proto);
173+
record.own = Promise.resolve("own property");
174+
175+
const result = await allKeyed(record);
176+
177+
assertEquals(Object.keys(result), ["own"]);
178+
assertFalse("inherited" in result);
179+
});
180+
181+
Deno.test("allKeyed() ignores non-enumerable symbol keys", async () => {
182+
const sym = Symbol("hidden");
183+
const record: Record<PropertyKey, Promise<string>> = {};
184+
Object.defineProperty(record, sym, {
185+
value: Promise.resolve("hidden"),
186+
enumerable: false,
187+
});
188+
Object.defineProperty(record, "visible", {
189+
value: Promise.resolve("visible"),
190+
enumerable: true,
191+
});
192+
193+
const result = await allKeyed(record);
194+
195+
assertEquals(Object.keys(result), ["visible"]);
196+
assertFalse(sym in result);
197+
});

0 commit comments

Comments
 (0)