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
3 changes: 2 additions & 1 deletion async/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"./unstable-throttle": "./unstable_throttle.ts",
"./unstable-wait-for": "./unstable_wait_for.ts",
"./unstable-semaphore": "./unstable_semaphore.ts",
"./unstable-circuit-breaker": "./unstable_circuit_breaker.ts"
"./unstable-circuit-breaker": "./unstable_circuit_breaker.ts",
"./unstable-all-keyed": "./unstable_all_keyed.ts"
}
}
184 changes: 184 additions & 0 deletions async/unstable_all_keyed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright 2018-2026 the Deno authors. MIT license.
// This module is browser compatible.

// TC39 spec uses [[OwnPropertyKeys]] (Reflect.ownKeys) then filters for enumerable.
// This equivalent is faster on V8: Object.keys returns only enumerable string keys,
// so we only need to filter symbol keys for enumerability. See also assert/equal.ts.
function getEnumerableKeys(obj: object): PropertyKey[] {
const stringKeys = Object.keys(obj);
const symbolKeys = Object.getOwnPropertySymbols(obj).filter((key) =>
Object.prototype.propertyIsEnumerable.call(obj, key)
);
return [...stringKeys, ...symbolKeys];
}

/**
* A record type where values can be promise-like (thenables) or plain values.
*
* @typeParam T The base record type with resolved value types.
*/
export type PromiseRecord<T extends Record<PropertyKey, unknown>> = {
[K in keyof T]: PromiseLike<T[K]> | T[K];
};

/**
* A record type where values are {@linkcode PromiseSettledResult} objects.
*
* @typeParam T The base record type with resolved value types.
*/
export type SettledRecord<T extends Record<PropertyKey, unknown>> = {
[K in keyof T]: PromiseSettledResult<T[K]>;
};

/**
* Resolves all values in a record of promises in parallel, returning a promise
* that resolves to a record with the same keys and resolved values.
*
* This is similar to {@linkcode Promise.all}, but for records instead of arrays,
* allowing you to use named keys instead of positional indices.
*
* If any promise rejects, the returned promise immediately rejects with the
* first rejection reason. The result object has a null prototype, matching the
* TC39 specification.
*
* This function implements the behavior proposed in the TC39
* {@link https://github.com/tc39/proposal-await-dictionary | Await Dictionary}
* proposal (`Promise.allKeyed`).
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @typeParam T The record shape with resolved (unwrapped) value types. For
* example, if passing `{ foo: Promise<number> }`, `T` would be `{ foo: number }`.
* @param record A record where values are promise-like (thenables) or plain values.
* @returns A promise that resolves to a record with the same keys and resolved
* values. The result has a null prototype.
* @throws Rejects with the first rejection reason if any promise in the record
* rejects.
*
* @example Basic usage
* ```ts
* import { allKeyed } from "@std/async/unstable-all-keyed";
* import { assertEquals } from "@std/assert";
*
* const result = await allKeyed({
* foo: Promise.resolve(1),
* bar: Promise.resolve("hello"),
* });
*
* assertEquals(result, { foo: 1, bar: "hello" });
* ```
*
* @example Parallel HTTP requests
* ```ts no-assert ignore
* import { allKeyed } from "@std/async/unstable-all-keyed";
*
* const { user, posts } = await allKeyed({
* user: fetch("/api/user").then((r) => r.json()),
* posts: fetch("/api/posts").then((r) => r.json()),
* });
* ```
*
* @example Mixed promises and plain values
* ```ts
* import { allKeyed } from "@std/async/unstable-all-keyed";
* import { assertEquals } from "@std/assert";
*
* const result = await allKeyed({
* promised: Promise.resolve(42),
* plain: "static",
* });
*
* assertEquals(result, { promised: 42, plain: "static" });
* ```
*/
export function allKeyed<T extends Record<PropertyKey, unknown>>(
record: PromiseRecord<T>,
): Promise<T> {
const keys = getEnumerableKeys(record);
const values = keys.map((key) => record[key as keyof typeof record]);

return Promise.all(values).then((resolved) => {
const result = Object.create(null) as T;
for (let i = 0; i < keys.length; i++) {
result[keys[i] as keyof T] = resolved[i] as T[keyof T];
}
return result;
});
}

/**
* Resolves all values in a record of promises in parallel, returning a promise
* that resolves to a record with the same keys and {@linkcode PromiseSettledResult}
* objects as values.
*
* This is similar to {@linkcode Promise.allSettled}, but for records instead of
* arrays, allowing you to use named keys instead of positional indices.
*
* Unlike {@linkcode allKeyed}, this function never rejects due to promise
* rejections. Instead, each value in the result record is a
* {@linkcode PromiseSettledResult} object indicating whether the corresponding
* promise was fulfilled or rejected. The result object has a null prototype,
* matching the TC39 specification.
*
* This function implements the behavior proposed in the TC39
* {@link https://github.com/tc39/proposal-await-dictionary | Await Dictionary}
* proposal (`Promise.allSettledKeyed`).
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @typeParam T The record shape with resolved (unwrapped) value types. For
* example, if passing `{ foo: Promise<number> }`, `T` would be `{ foo: number }`.
* @param record A record where values are promise-like (thenables) or plain values.
* @returns A promise that resolves to a record with the same keys and
* {@linkcode PromiseSettledResult} values. The result has a null prototype.
*
* @example Basic usage
* ```ts
* import { allSettledKeyed } from "@std/async/unstable-all-keyed";
* import { assertEquals } from "@std/assert";
*
* const settled = await allSettledKeyed({
* success: Promise.resolve(1),
* failure: Promise.reject(new Error("oops")),
* });
*
* assertEquals(settled.success, { status: "fulfilled", value: 1 });
* assertEquals(settled.failure.status, "rejected");
* ```
*
* @example Error handling
* ```ts
* import { allSettledKeyed } from "@std/async/unstable-all-keyed";
* import { assertEquals, assertExists } from "@std/assert";
*
* const settled = await allSettledKeyed({
* a: Promise.resolve("ok"),
* b: Promise.reject(new Error("fail")),
* c: Promise.resolve("also ok"),
* });
*
* // Check individual results
* if (settled.a.status === "fulfilled") {
* assertEquals(settled.a.value, "ok");
* }
* if (settled.b.status === "rejected") {
* assertExists(settled.b.reason);
* }
* ```
*/
export function allSettledKeyed<T extends Record<PropertyKey, unknown>>(
record: PromiseRecord<T>,
): Promise<SettledRecord<T>> {
const keys = getEnumerableKeys(record);
const values = keys.map((key) => record[key as keyof typeof record]);

return Promise.allSettled(values).then((settled) => {
const result = Object.create(null) as SettledRecord<T>;
for (let i = 0; i < keys.length; i++) {
result[keys[i] as keyof T] = settled[i] as PromiseSettledResult<
T[keyof T]
>;
}
return result;
});
}
197 changes: 197 additions & 0 deletions async/unstable_all_keyed_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// Copyright 2018-2026 the Deno authors. MIT license.
import { allKeyed, allSettledKeyed } from "./unstable_all_keyed.ts";
import {
assertEquals,
assertFalse,
assertRejects,
assertStrictEquals,
} from "@std/assert";

// allKeyed tests

Deno.test("allKeyed() resolves record of promises", async () => {
const result = await allKeyed({
a: Promise.resolve(1),
b: Promise.resolve("two"),
c: Promise.resolve(true),
});

assertEquals(result, { a: 1, b: "two", c: true });
});

Deno.test("allKeyed() handles mixed promises and plain values", async () => {
const result = await allKeyed({
promise: Promise.resolve(42),
plain: "static",
anotherPromise: Promise.resolve([1, 2, 3]),
});

assertEquals(result, {
promise: 42,
plain: "static",
anotherPromise: [1, 2, 3],
});
});

Deno.test("allKeyed() handles empty record", async () => {
const result = await allKeyed({});
assertEquals(result, {});
});

Deno.test("allKeyed() rejects on first rejection", async () => {
const error = new Error("test error");

await assertRejects(
() =>
allKeyed({
a: Promise.resolve(1),
b: Promise.reject(error),
c: Promise.resolve(3),
}),
Error,
"test error",
);
});

Deno.test("allKeyed() preserves symbol keys", async () => {
const sym = Symbol("test");
const result = await allKeyed({
[sym]: Promise.resolve("symbol value"),
regular: Promise.resolve("regular value"),
});

assertEquals(result[sym], "symbol value");
assertEquals(result.regular, "regular value");
});

Deno.test("allKeyed() ignores non-enumerable properties", async () => {
const record = Object.create(null);
Object.defineProperty(record, "enumerable", {
value: Promise.resolve("visible"),
enumerable: true,
});
Object.defineProperty(record, "nonEnumerable", {
value: Promise.resolve("hidden"),
enumerable: false,
});

const result = await allKeyed(record);

assertEquals(result, { enumerable: "visible" });
assertEquals(Object.keys(result), ["enumerable"]);
});

// allSettledKeyed tests

Deno.test("allSettledKeyed() resolves all promises", async () => {
const result = await allSettledKeyed({
a: Promise.resolve(1),
b: Promise.resolve("two"),
});

assertEquals(result, {
a: { status: "fulfilled", value: 1 },
b: { status: "fulfilled", value: "two" },
});
});

Deno.test("allSettledKeyed() handles mixed fulfilled and rejected", async () => {
const error = new Error("rejection reason");
const result = await allSettledKeyed({
success: Promise.resolve("ok"),
failure: Promise.reject(error),
});

assertEquals(result.success, { status: "fulfilled", value: "ok" });
assertEquals(result.failure.status, "rejected");
assertEquals((result.failure as PromiseRejectedResult).reason, error);
});

Deno.test("allSettledKeyed() handles all rejections without throwing", async () => {
const error1 = new Error("error 1");
const error2 = new Error("error 2");

const result = await allSettledKeyed({
a: Promise.reject(error1),
b: Promise.reject(error2),
});

assertEquals(result.a.status, "rejected");
assertEquals(result.b.status, "rejected");
assertEquals((result.a as PromiseRejectedResult).reason, error1);
assertEquals((result.b as PromiseRejectedResult).reason, error2);
});

Deno.test("allSettledKeyed() handles empty record", async () => {
const result = await allSettledKeyed({});
assertEquals(result, {});
});

Deno.test("allSettledKeyed() preserves symbol keys", async () => {
const sym = Symbol("test");
const result = await allSettledKeyed({
[sym]: Promise.resolve("symbol"),
regular: Promise.reject(new Error("fail")),
});

assertEquals(result[sym], { status: "fulfilled", value: "symbol" });
assertEquals(result.regular.status, "rejected");
});

Deno.test("allKeyed() returns object with null prototype", async () => {
const result = await allKeyed({ a: Promise.resolve(1) });

assertStrictEquals(Object.getPrototypeOf(result), null);
assertFalse("hasOwnProperty" in result);
assertFalse("toString" in result);
});

Deno.test("allSettledKeyed() returns object with null prototype", async () => {
const result = await allSettledKeyed({ a: Promise.resolve(1) });

assertStrictEquals(Object.getPrototypeOf(result), null);
assertFalse("hasOwnProperty" in result);
assertFalse("toString" in result);
});

Deno.test("allKeyed() preserves numeric key order", async () => {
const result = await allKeyed({
"2": Promise.resolve("two"),
"1": Promise.resolve("one"),
"10": Promise.resolve("ten"),
});

assertEquals(Object.keys(result), ["1", "2", "10"]);
assertEquals(result["1"], "one");
assertEquals(result["2"], "two");
assertEquals(result["10"], "ten");
});

Deno.test("allKeyed() ignores inherited properties", async () => {
const proto = { inherited: Promise.resolve("from proto") };
const record = Object.create(proto);
record.own = Promise.resolve("own property");

const result = await allKeyed(record);

assertEquals(Object.keys(result), ["own"]);
assertFalse("inherited" in result);
});

Deno.test("allKeyed() ignores non-enumerable symbol keys", async () => {
const sym = Symbol("hidden");
const record: Record<PropertyKey, Promise<string>> = {};
Object.defineProperty(record, sym, {
value: Promise.resolve("hidden"),
enumerable: false,
});
Object.defineProperty(record, "visible", {
value: Promise.resolve("visible"),
enumerable: true,
});

const result = await allKeyed(record);

assertEquals(Object.keys(result), ["visible"]);
assertFalse(sym in result);
});
Loading