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
55 changes: 55 additions & 0 deletions docs/ja/reference/promise/allKeyed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# allKeyed

Promiseのオブジェクトを並行して解決し、同じキーと解決された値を持つオブジェクトを返します。

```typescript
await allKeyed(tasks);
```

## 使い方

### `allKeyed(tasks)`

複数のPromiseを並行して実行し、位置インデックスではなく名前で結果にアクセスしたい場合に`allKeyed`を使用します。`Promise.all`と似ていますが、配列の代わりにオブジェクトのPromiseを受け取り、結果にキーを保持します。

[TC39 `Promise.allKeyed` 提案](https://github.com/tc39/proposal-await-dictionary)に基づいています。

```typescript
import { allKeyed } from 'es-toolkit/promise';

const { user, posts } = await allKeyed({
user: fetchUser(),
posts: fetchPosts(),
});
```

プレーンな値もPromiseと一緒にサポートされています。

```typescript
const result = await allKeyed({
a: Promise.resolve(1),
b: 2,
});
// { a: 1, b: 2 }
```

配列の順序を気にせずに複数のリソースを並行して取得する場合にも便利です。

```typescript
// Promise.allでは順序を入れ替えると分割代入が静かに壊れます:
// const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);

// allKeyedではキーが明示的なので順序バグがありません:
const { user, posts } = await allKeyed({
user: fetchUser(),
posts: fetchPosts(),
});
```

#### パラメータ

- `tasks` (`T`): 並行して解決するPromise(またはプレーンな値)を値として持つオブジェクトです。

#### 戻り値

(`Promise<{ [K in keyof T]: Awaited<T[K]> }>`): 同じキーと解決された値を持つオブジェクトに解決されるPromiseを返します。
55 changes: 55 additions & 0 deletions docs/ko/reference/promise/allKeyed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# allKeyed

Promise 객체를 동시에 실행하고, 같은 키와 해결된 값을 가진 객체를 반환해요.

```typescript
await allKeyed(tasks);
```

## 사용법

### `allKeyed(tasks)`

여러 Promise를 병렬로 실행하고, 위치 인덱스가 아닌 이름으로 결과에 접근하고 싶을 때 `allKeyed`를 사용하세요. `Promise.all`과 비슷하지만, 배열 대신 객체의 Promise를 받아서 결과에서 키를 유지해요.

[TC39 `Promise.allKeyed` 제안](https://github.com/tc39/proposal-await-dictionary)에 기반해요.

```typescript
import { allKeyed } from 'es-toolkit/promise';

const { user, posts } = await allKeyed({
user: fetchUser(),
posts: fetchPosts(),
});
```

Promise와 함께 일반 값도 지원해요.

```typescript
const result = await allKeyed({
a: Promise.resolve(1),
b: 2,
});
// { a: 1, b: 2 }
```

배열 순서를 신경 쓰지 않고 여러 리소스를 병렬로 가져올 때도 유용해요.

```typescript
// Promise.all은 순서가 바뀌면 구조 분해가 조용히 깨져요:
// const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);

// allKeyed는 키가 명시적이라 순서 버그가 없어요:
const { user, posts } = await allKeyed({
user: fetchUser(),
posts: fetchPosts(),
});
```

#### 파라미터

- `tasks` (`T`): 동시에 실행할 Promise(또는 일반 값)를 값으로 가진 객체예요.

#### 반환 값

(`Promise<{ [K in keyof T]: Awaited<T[K]> }>`): 같은 키와 해결된 값을 가진 객체로 해결되는 Promise를 반환해요.
55 changes: 55 additions & 0 deletions docs/reference/promise/allKeyed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# allKeyed

Resolves an object of promises concurrently, returning an object with the same keys and resolved values.

```typescript
await allKeyed(tasks);
```

## Usage

### `allKeyed(tasks)`

Use `allKeyed` when you want to run multiple promises in parallel and access results by name instead of positional indices. Similar to `Promise.all`, but accepts an object of promises and preserves keys in the result.

Based on the [TC39 `Promise.allKeyed` proposal](https://github.com/tc39/proposal-await-dictionary).

```typescript
import { allKeyed } from 'es-toolkit/promise';

const { user, posts } = await allKeyed({
user: fetchUser(),
posts: fetchPosts(),
});
```

Plain values are also supported alongside promises.

```typescript
const result = await allKeyed({
a: Promise.resolve(1),
b: 2,
});
// { a: 1, b: 2 }
```

It's also useful when you want to fetch multiple resources in parallel without worrying about array ordering.

```typescript
// With Promise.all, swapping the order silently breaks destructuring:
// const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);

// With allKeyed, keys are explicit — no ordering bugs:
const { user, posts } = await allKeyed({
user: fetchUser(),
posts: fetchPosts(),
});
```

#### Parameters

- `tasks` (`T`): An object whose values are promises (or plain values) to resolve concurrently.

#### Returns

(`Promise<{ [K in keyof T]: Awaited<T[K]> }>`): A promise that resolves to an object with the same keys and resolved values.
55 changes: 55 additions & 0 deletions docs/zh_hans/reference/promise/allKeyed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# allKeyed

并发解析一个 Promise 对象,返回一个具有相同键和解析值的对象。

```typescript
await allKeyed(tasks);
```

## 用法

### `allKeyed(tasks)`

当您想要并行运行多个 Promise 并通过名称而不是位置索引访问结果时,可以使用 `allKeyed`。它类似于 `Promise.all`,但接受一个 Promise 对象而不是数组,在结果中保留键。

基于 [TC39 `Promise.allKeyed` 提案](https://github.com/tc39/proposal-await-dictionary)。

```typescript
import { allKeyed } from 'es-toolkit/promise';

const { user, posts } = await allKeyed({
user: fetchUser(),
posts: fetchPosts(),
});
```

也支持与 Promise 一起使用普通值。

```typescript
const result = await allKeyed({
a: Promise.resolve(1),
b: 2,
});
// { a: 1, b: 2 }
```

当您想要并行获取多个资源而不必担心数组顺序时也很有用。

```typescript
// 使用 Promise.all 时,交换顺序会静默破坏解构:
// const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);

// 使用 allKeyed 时,键是显式的——没有顺序错误:
const { user, posts } = await allKeyed({
user: fetchUser(),
posts: fetchPosts(),
});
```

#### 参数

- `tasks` (`T`): 一个对象,其值为要并发解析的 Promise(或普通值)。

#### 返回值

(`Promise<{ [K in keyof T]: Awaited<T[K]> }>`): 返回一个 Promise,解析为具有相同键和解析值的对象。
73 changes: 73 additions & 0 deletions src/promise/allKeyed.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import { allKeyed } from './allKeyed';

describe('allKeyed', () => {
it('should resolve an object of promises concurrently', async () => {
const result = await allKeyed({
a: Promise.resolve(1),
b: Promise.resolve('hello'),
c: Promise.resolve(true),
});

expect(result).toEqual({ a: 1, b: 'hello', c: true });
});

it('should handle plain (non-promise) values', async () => {
const result = await allKeyed({
a: 1,
b: 'hello',
});

expect(result).toEqual({ a: 1, b: 'hello' });
});

it('should handle a mix of promises and plain values', async () => {
const result = await allKeyed({
a: Promise.resolve(1),
b: 'plain',
});

expect(result).toEqual({ a: 1, b: 'plain' });
});

it('should return an empty object for empty input', async () => {
const result = await allKeyed({});

expect(result).toEqual({});
});

it('should reject if any promise rejects', async () => {
await expect(
allKeyed({
a: Promise.resolve(1),
b: Promise.reject(new Error('fail')),
})
).rejects.toThrow('fail');
});
Comment on lines +33 to +46
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the library already has prototype-pollution hardening elsewhere (e.g., skipping/handling __proto__), it would be good to add a test case where the input contains a __proto__ key and assert it becomes an own enumerable property (and does not alter the returned object's prototype). This will prevent regressions once the implementation is hardened.

Copilot uses AI. Check for mistakes.

it('should resolve promises concurrently, not sequentially', async () => {
const start = Date.now();

await allKeyed({
a: new Promise(resolve => setTimeout(() => resolve('a'), 50)),
b: new Promise(resolve => setTimeout(() => resolve('b'), 50)),
});

const elapsed = Date.now() - start;

// If run sequentially, would take ~100ms. Concurrently should be ~50ms.
expect(elapsed).toBeLessThan(90);
});
Comment on lines +48 to +60
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concurrency assertion relies on real wall-clock timing (Date.now() and setTimeout) with a fairly tight threshold, which is likely to be flaky under CI load. Consider using Vitest fake timers (vi.useFakeTimers() / advanceTimersByTime) or at least a more robust timing approach (e.g., performance.now() + a looser bound) so the test validates concurrency without depending on scheduler jitter.

Copilot uses AI. Check for mistakes.

it('should preserve key-value associations', async () => {
const result = await allKeyed({
first: Promise.resolve(1),
second: Promise.resolve(2),
third: Promise.resolve(3),
});

expect(result.first).toBe(1);
expect(result.second).toBe(2);
expect(result.third).toBe(3);
});
});
41 changes: 41 additions & 0 deletions src/promise/allKeyed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Resolves an object of promises concurrently, returning an object with the same keys and resolved values.
*
* Similar to `Promise.all`, but accepts an object of promises instead of an array,
* preserving the keys in the result. This makes it easy to destructure the resolved values
* by name instead of relying on positional indices.
*
* Based on the [TC39 `Promise.allKeyed` proposal](https://github.com/tc39/proposal-await-dictionary).
*
* @template T - A record type where each value is a promise or a value.
* @param {T} tasks - An object whose values are promises (or plain values) to resolve concurrently.
* @returns {Promise<{ [K in keyof T]: Awaited<T[K]> }>} A promise that resolves to an object with the same keys and resolved values.
*
* @example
* const { user, posts } = await allKeyed({
* user: fetchUser(),
* posts: fetchPosts(),
* });
*
* @example
* // Plain values are also supported
* const result = await allKeyed({
* a: Promise.resolve(1),
* b: 2,
* });
* // { a: 1, b: 2 }
*/
export async function allKeyed<T extends Record<string, unknown>>(
tasks: T
): Promise<{ [K in keyof T]: Awaited<T[K]> }> {
const keys = Object.keys(tasks) as Array<keyof T>;
const values = await Promise.all(keys.map(key => tasks[key]));

const result = {} as { [K in keyof T]: Awaited<T[K]> };

for (let i = 0; i < keys.length; i++) {
result[keys[i]] = values[i] as Awaited<T[(typeof keys)[number]]>;
}
Comment on lines +34 to +38
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Building result as a normal object and assigning result[key] = ... can trigger the special __proto__ setter when the input contains a __proto__ key, changing the returned object's prototype (prototype pollution risk / surprising behavior). Consider using the existing _internal/isUnsafeProperty guard and Object.defineProperty for __proto__ (similar to toPlainObject), or otherwise ensure __proto__ becomes a normal data property.

Copilot uses AI. Check for mistakes.

return result;
}
1 change: 1 addition & 0 deletions src/promise/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { allKeyed } from './allKeyed.ts';
export { delay } from './delay.ts';
export { Mutex } from './mutex.ts';
export { Semaphore } from './semaphore.ts';
Expand Down
Loading