diff --git a/docs/ja/reference/object/mapKeysAsync.md b/docs/ja/reference/object/mapKeysAsync.md new file mode 100644 index 000000000..5c115f430 --- /dev/null +++ b/docs/ja/reference/object/mapKeysAsync.md @@ -0,0 +1,45 @@ +# mapKeysAsync + +非同期関数を通じてキーを変換した新しいオブジェクトを返します。 + +```typescript +const newObj = await mapKeysAsync(object, getNewKey); +``` + +## 使用方法 + +### `mapKeysAsync(object, getNewKey, options?)` + +各キーを非同期に変換して新しいオブジェクトを作成する場合に`mapKeysAsync`を使用します。値はそのまま保持され、キーのみが`getNewKey`関数の解決結果に変更されます。 + +```typescript +import { mapKeysAsync } from 'es-toolkit/object'; + +// キーにプレフィックスを追加 +const obj = { a: 1, b: 2 }; +const prefixed = await mapKeysAsync(obj, (value, key) => `prefix_${key}`); +// prefixed は { prefix_a: 1, prefix_b: 2 } になる + +// キーと値を組み合わせて新しいキーを作成 +const combined = await mapKeysAsync(obj, (value, key) => `${key}${value}`); +// combined は { a1: 1, b2: 2 } になる + +// キーを大文字に変換 +const uppercased = await mapKeysAsync(obj, (value, key) => key.toString().toUpperCase()); +// uppercased は { A: 1, B: 2 } になる + +// 並行処理数を制限 +await mapKeysAsync(obj, async (value, key) => await processKey(key, value), { concurrency: 2 }); +// 最大2つのキーが同時に処理される +``` + +#### パラメータ + +- `object` (`T extends Record`): キーを変換する元のオブジェクト。 +- `getNewKey` (`(value: T[keyof T], key: keyof T, object: T) => Promise`): 新しいキーを生成する非同期関数。値、キー、オブジェクト全体をパラメータとして受け取る。 +- `options` (`MapKeysAsyncOptions`, 省略可): 並行処理数を制御するオプション。 + - `concurrency` (`number`, 省略可): 並行処理の最大数。指定しない場合、すべての操作が同時に実行される。 + +#### 戻り値 + +(`Promise>`): 変換されたキーを持つ新しいオブジェクトに解決されるプロミス。 diff --git a/docs/ja/reference/object/mapValuesAsync.md b/docs/ja/reference/object/mapValuesAsync.md new file mode 100644 index 000000000..d30469853 --- /dev/null +++ b/docs/ja/reference/object/mapValuesAsync.md @@ -0,0 +1,48 @@ +# mapValuesAsync + +非同期関数を通じて値を変換した新しいオブジェクトを返します。 + +```typescript +const newObj = await mapValuesAsync(object, getNewValue); +``` + +## 使用方法 + +### `mapValuesAsync(object, getNewValue, options?)` + +各値を非同期に変換して新しいオブジェクトを作成する場合に`mapValuesAsync`を使用します。キーはそのまま保持され、値のみが`getNewValue`関数の解決結果に変更されます。 + +```typescript +import { mapValuesAsync } from 'es-toolkit/object'; + +// すべての値を2倍にする +const numbers = { a: 1, b: 2, c: 3 }; +const doubled = await mapValuesAsync(numbers, async value => value * 2); +// doubled は { a: 2, b: 4, c: 6 } になる + +// 文字列値を大文字に変換 +const strings = { first: 'hello', second: 'world' }; +const uppercased = await mapValuesAsync(strings, async value => value.toUpperCase()); +// uppercased は { first: 'HELLO', second: 'WORLD' } になる + +// キーと値の両方を使用 +const scores = { alice: 85, bob: 90, charlie: 95 }; +const grades = await mapValuesAsync(scores, async (value, key) => `${key}: ${value >= 90 ? 'A' : 'B'}`); +// grades は { alice: 'alice: B', bob: 'bob: A', charlie: 'charlie: A' } になる + +// 並行処理数を制限 +const items = { a: 1, b: 2, c: 3 }; +await mapValuesAsync(items, async item => await processItem(item), { concurrency: 2 }); +// 最大2つの値が同時に処理される +``` + +#### パラメータ + +- `object` (`T extends object`): 値を変換する元のオブジェクト。 +- `getNewValue` (`(value: T[K], key: K, object: T) => Promise`): 新しい値を生成する非同期関数。値、キー、オブジェクト全体をパラメータとして受け取る。 +- `options` (`MapValuesAsyncOptions`, 省略可): 並行処理数を制御するオプション。 + - `concurrency` (`number`, 省略可): 並行処理の最大数。指定しない場合、すべての操作が同時に実行される。 + +#### 戻り値 + +(`Promise>`): 変換された値を持つ新しいオブジェクトに解決されるプロミス。 diff --git a/docs/ko/reference/object/mapKeysAsync.md b/docs/ko/reference/object/mapKeysAsync.md new file mode 100644 index 000000000..9154c4c75 --- /dev/null +++ b/docs/ko/reference/object/mapKeysAsync.md @@ -0,0 +1,45 @@ +# mapKeysAsync + +비동기 함수를 통해 키를 변환한 새 객체를 반환해요. + +```typescript +const newObj = await mapKeysAsync(object, getNewKey); +``` + +## 사용법 + +### `mapKeysAsync(object, getNewKey, options?)` + +각 키를 비동기적으로 변환하여 새 객체를 만들 때 `mapKeysAsync`를 사용하세요. 값은 그대로 유지되며 키만 `getNewKey` 함수의 해결 결과로 변경돼요. + +```typescript +import { mapKeysAsync } from 'es-toolkit/object'; + +// 키에 접두사 추가 +const obj = { a: 1, b: 2 }; +const prefixed = await mapKeysAsync(obj, (value, key) => `prefix_${key}`); +// prefixed는 { prefix_a: 1, prefix_b: 2 }가 됨 + +// 키와 값을 결합하여 새 키 생성 +const combined = await mapKeysAsync(obj, (value, key) => `${key}${value}`); +// combined는 { a1: 1, b2: 2 }가 됨 + +// 키를 대문자로 변환 +const uppercased = await mapKeysAsync(obj, (value, key) => key.toString().toUpperCase()); +// uppercased는 { A: 1, B: 2 }가 됨 + +// 동시 실행 수 제한 +await mapKeysAsync(obj, async (value, key) => await processKey(key, value), { concurrency: 2 }); +// 최대 2개의 키가 동시에 처리됨 +``` + +#### 매개변수 + +- `object` (`T extends Record`): 키를 변환할 원본 객체예요. +- `getNewKey` (`(value: T[keyof T], key: keyof T, object: T) => Promise`): 새 키를 생성하는 비동기 함수예요. 값, 키, 전체 객체를 매개변수로 받아요. +- `options` (`MapKeysAsyncOptions`, 선택사항): 동시 실행 수를 제어하는 옵션이에요. + - `concurrency` (`number`, 선택사항): 최대 동시 실행 수예요. 지정하지 않으면 모든 작업이 동시에 실행돼요. + +#### 반환값 + +(`Promise>`): 변환된 키를 가진 새 객체로 해결되는 프로미스예요. diff --git a/docs/ko/reference/object/mapValuesAsync.md b/docs/ko/reference/object/mapValuesAsync.md new file mode 100644 index 000000000..39c90f34b --- /dev/null +++ b/docs/ko/reference/object/mapValuesAsync.md @@ -0,0 +1,48 @@ +# mapValuesAsync + +비동기 함수를 통해 값을 변환한 새 객체를 반환해요. + +```typescript +const newObj = await mapValuesAsync(object, getNewValue); +``` + +## 사용법 + +### `mapValuesAsync(object, getNewValue, options?)` + +각 값을 비동기적으로 변환하여 새 객체를 만들 때 `mapValuesAsync`를 사용하세요. 키는 그대로 유지되며 값만 `getNewValue` 함수의 해결 결과로 변경돼요. + +```typescript +import { mapValuesAsync } from 'es-toolkit/object'; + +// 모든 값을 2배로 만들기 +const numbers = { a: 1, b: 2, c: 3 }; +const doubled = await mapValuesAsync(numbers, async value => value * 2); +// doubled는 { a: 2, b: 4, c: 6 }이 됨 + +// 문자열 값을 대문자로 변환 +const strings = { first: 'hello', second: 'world' }; +const uppercased = await mapValuesAsync(strings, async value => value.toUpperCase()); +// uppercased는 { first: 'HELLO', second: 'WORLD' }가 됨 + +// 키와 값 모두 사용 +const scores = { alice: 85, bob: 90, charlie: 95 }; +const grades = await mapValuesAsync(scores, async (value, key) => `${key}: ${value >= 90 ? 'A' : 'B'}`); +// grades는 { alice: 'alice: B', bob: 'bob: A', charlie: 'charlie: A' }가 됨 + +// 동시 실행 수 제한 +const items = { a: 1, b: 2, c: 3 }; +await mapValuesAsync(items, async item => await processItem(item), { concurrency: 2 }); +// 최대 2개의 값이 동시에 처리됨 +``` + +#### 매개변수 + +- `object` (`T extends object`): 값을 변환할 원본 객체예요. +- `getNewValue` (`(value: T[K], key: K, object: T) => Promise`): 새 값을 생성하는 비동기 함수예요. 값, 키, 전체 객체를 매개변수로 받아요. +- `options` (`MapValuesAsyncOptions`, 선택사항): 동시 실행 수를 제어하는 옵션이에요. + - `concurrency` (`number`, 선택사항): 최대 동시 실행 수예요. 지정하지 않으면 모든 작업이 동시에 실행돼요. + +#### 반환값 + +(`Promise>`): 변환된 값을 가진 새 객체로 해결되는 프로미스예요. diff --git a/docs/reference/object/mapKeysAsync.md b/docs/reference/object/mapKeysAsync.md new file mode 100644 index 000000000..198777098 --- /dev/null +++ b/docs/reference/object/mapKeysAsync.md @@ -0,0 +1,45 @@ +# mapKeysAsync + +Returns a new object with keys transformed through an async function. + +```typescript +const newObj = await mapKeysAsync(object, getNewKey); +``` + +## Usage + +### `mapKeysAsync(object, getNewKey, options?)` + +Use `mapKeysAsync` when you want to create a new object by asynchronously transforming each key. Values remain the same, and only the keys are changed to the resolved results of the `getNewKey` function. + +```typescript +import { mapKeysAsync } from 'es-toolkit/object'; + +// Add prefix to keys +const obj = { a: 1, b: 2 }; +const prefixed = await mapKeysAsync(obj, (value, key) => `prefix_${key}`); +// prefixed becomes { prefix_a: 1, prefix_b: 2 } + +// Combine key and value to create new keys +const combined = await mapKeysAsync(obj, (value, key) => `${key}${value}`); +// combined becomes { a1: 1, b2: 2 } + +// Convert keys to uppercase +const uppercased = await mapKeysAsync(obj, (value, key) => key.toString().toUpperCase()); +// uppercased becomes { A: 1, B: 2 } + +// Limit concurrency. +await mapKeysAsync(obj, async (value, key) => await processKey(key, value), { concurrency: 2 }); +// Only 2 keys are processed concurrently at most. +``` + +#### Parameters + +- `object` (`T extends Record`): The object to transform keys from. +- `getNewKey` (`(value: T[keyof T], key: keyof T, object: T) => Promise`): An async function that generates new keys. Receives value, key, and the entire object as parameters. +- `options` (`MapKeysAsyncOptions`, optional): Options to control concurrency. + - `concurrency` (`number`, optional): Maximum number of concurrent operations. If not specified, all operations run concurrently. + +#### Returns + +(`Promise>`): A promise that resolves to a new object with transformed keys. diff --git a/docs/reference/object/mapValuesAsync.md b/docs/reference/object/mapValuesAsync.md new file mode 100644 index 000000000..13178fb57 --- /dev/null +++ b/docs/reference/object/mapValuesAsync.md @@ -0,0 +1,48 @@ +# mapValuesAsync + +Returns a new object with values transformed through an async function. + +```typescript +const newObj = await mapValuesAsync(object, getNewValue); +``` + +## Usage + +### `mapValuesAsync(object, getNewValue, options?)` + +Use `mapValuesAsync` when you want to create a new object by asynchronously transforming each value. Keys remain the same, and only the values are changed to the resolved results of the `getNewValue` function. + +```typescript +import { mapValuesAsync } from 'es-toolkit/object'; + +// Double all values +const numbers = { a: 1, b: 2, c: 3 }; +const doubled = await mapValuesAsync(numbers, async value => value * 2); +// doubled becomes { a: 2, b: 4, c: 6 } + +// Convert string values to uppercase +const strings = { first: 'hello', second: 'world' }; +const uppercased = await mapValuesAsync(strings, async value => value.toUpperCase()); +// uppercased becomes { first: 'HELLO', second: 'WORLD' } + +// Use both key and value +const scores = { alice: 85, bob: 90, charlie: 95 }; +const grades = await mapValuesAsync(scores, async (value, key) => `${key}: ${value >= 90 ? 'A' : 'B'}`); +// grades becomes { alice: 'alice: B', bob: 'bob: A', charlie: 'charlie: A' } + +// Limit concurrency. +const items = { a: 1, b: 2, c: 3 }; +await mapValuesAsync(items, async item => await processItem(item), { concurrency: 2 }); +// Only 2 values are processed concurrently at most. +``` + +#### Parameters + +- `object` (`T extends object`): The object to transform values from. +- `getNewValue` (`(value: T[K], key: K, object: T) => Promise`): An async function that generates new values. Receives value, key, and the entire object as parameters. +- `options` (`MapValuesAsyncOptions`, optional): Options to control concurrency. + - `concurrency` (`number`, optional): Maximum number of concurrent operations. If not specified, all operations run concurrently. + +#### Returns + +(`Promise>`): A promise that resolves to a new object with transformed values. diff --git a/docs/zh_hans/reference/object/mapKeysAsync.md b/docs/zh_hans/reference/object/mapKeysAsync.md new file mode 100644 index 000000000..5a588863e --- /dev/null +++ b/docs/zh_hans/reference/object/mapKeysAsync.md @@ -0,0 +1,45 @@ +# mapKeysAsync + +返回一个通过异步函数转换键后的新对象。 + +```typescript +const newObj = await mapKeysAsync(object, getNewKey); +``` + +## 用法 + +### `mapKeysAsync(object, getNewKey, options?)` + +当您想要通过异步转换每个键来创建新对象时使用`mapKeysAsync`。值保持不变,只有键会更改为`getNewKey`函数的解析结果。 + +```typescript +import { mapKeysAsync } from 'es-toolkit/object'; + +// 为键添加前缀 +const obj = { a: 1, b: 2 }; +const prefixed = await mapKeysAsync(obj, (value, key) => `prefix_${key}`); +// prefixed 变为 { prefix_a: 1, prefix_b: 2 } + +// 组合键和值来创建新键 +const combined = await mapKeysAsync(obj, (value, key) => `${key}${value}`); +// combined 变为 { a1: 1, b2: 2 } + +// 将键转换为大写 +const uppercased = await mapKeysAsync(obj, (value, key) => key.toString().toUpperCase()); +// uppercased 变为 { A: 1, B: 2 } + +// 限制并发数 +await mapKeysAsync(obj, async (value, key) => await processKey(key, value), { concurrency: 2 }); +// 最多同时处理2个键 +``` + +#### 参数 + +- `object` (`T extends Record`): 要转换键的源对象。 +- `getNewKey` (`(value: T[keyof T], key: keyof T, object: T) => Promise`): 生成新键的异步函数。接收值、键和整个对象作为参数。 +- `options` (`MapKeysAsyncOptions`, 可选): 控制并发的选项。 + - `concurrency` (`number`, 可选): 最大并发操作数。如果未指定,所有操作将并发运行。 + +#### 返回值 + +(`Promise>`): 解析为具有转换后键的新对象的 Promise。 diff --git a/docs/zh_hans/reference/object/mapValuesAsync.md b/docs/zh_hans/reference/object/mapValuesAsync.md new file mode 100644 index 000000000..0bc3e32c2 --- /dev/null +++ b/docs/zh_hans/reference/object/mapValuesAsync.md @@ -0,0 +1,48 @@ +# mapValuesAsync + +返回一个通过异步函数转换值后的新对象。 + +```typescript +const newObj = await mapValuesAsync(object, getNewValue); +``` + +## 用法 + +### `mapValuesAsync(object, getNewValue, options?)` + +当您想要通过异步转换每个值来创建新对象时使用`mapValuesAsync`。键保持不变,只有值会更改为`getNewValue`函数的解析结果。 + +```typescript +import { mapValuesAsync } from 'es-toolkit/object'; + +// 将所有值翻倍 +const numbers = { a: 1, b: 2, c: 3 }; +const doubled = await mapValuesAsync(numbers, async value => value * 2); +// doubled 变为 { a: 2, b: 4, c: 6 } + +// 将字符串值转换为大写 +const strings = { first: 'hello', second: 'world' }; +const uppercased = await mapValuesAsync(strings, async value => value.toUpperCase()); +// uppercased 变为 { first: 'HELLO', second: 'WORLD' } + +// 同时使用键和值 +const scores = { alice: 85, bob: 90, charlie: 95 }; +const grades = await mapValuesAsync(scores, async (value, key) => `${key}: ${value >= 90 ? 'A' : 'B'}`); +// grades 变为 { alice: 'alice: B', bob: 'bob: A', charlie: 'charlie: A' } + +// 限制并发数 +const items = { a: 1, b: 2, c: 3 }; +await mapValuesAsync(items, async item => await processItem(item), { concurrency: 2 }); +// 最多同时处理2个值 +``` + +#### 参数 + +- `object` (`T extends object`): 要转换值的源对象。 +- `getNewValue` (`(value: T[K], key: K, object: T) => Promise`): 生成新值的异步函数。接收值、键和整个对象作为参数。 +- `options` (`MapValuesAsyncOptions`, 可选): 控制并发的选项。 + - `concurrency` (`number`, 可选): 最大并发操作数。如果未指定,所有操作将并发运行。 + +#### 返回值 + +(`Promise>`): 解析为具有转换后值的新对象的 Promise。 diff --git a/src/object/index.ts b/src/object/index.ts index 1c75c7bdf..b6f15b960 100644 --- a/src/object/index.ts +++ b/src/object/index.ts @@ -5,7 +5,9 @@ export { findKey } from './findKey.ts'; export { flattenObject } from './flattenObject.ts'; export { invert } from './invert.ts'; export { mapKeys } from './mapKeys.ts'; +export { mapKeysAsync } from './mapKeysAsync.ts'; export { mapValues } from './mapValues.ts'; +export { mapValuesAsync } from './mapValuesAsync.ts'; export { merge } from './merge.ts'; export { mergeWith } from './mergeWith.ts'; export { omit } from './omit.ts'; diff --git a/src/object/mapKeysAsync.spec.ts b/src/object/mapKeysAsync.spec.ts new file mode 100644 index 000000000..7941df5be --- /dev/null +++ b/src/object/mapKeysAsync.spec.ts @@ -0,0 +1,76 @@ +import { describe, expect, it, vi } from 'vitest'; +import { mapKeysAsync } from './mapKeysAsync.ts'; +import { delay } from '../promise/delay.ts'; + +describe('mapKeysAsync', () => { + it('should iterate over and map the object using its own string keys asynchronously', async () => { + const result = await mapKeysAsync({ a: 1, b: 2, c: 3 }, async (_, key) => `${key}a`); + expect(result).toEqual({ aa: 1, ba: 2, ca: 3 }); + }); + + it('should iterate over and map the object using its own number keys asynchronously', async () => { + const result = await mapKeysAsync({ 1: 'a', 2: 'b', 3: 'c' }, async (_, key) => key * 2); + expect(result).toEqual({ 2: 'a', 4: 'b', 6: 'c' }); + }); + + it('should pass the value corresponding to the current key into the iteratee', async () => { + const result = await mapKeysAsync({ a: 1, b: 2, c: 3 }, async value => value); + expect(result).toEqual({ 1: 1, 2: 2, 3: 3 }); + }); + + it('should propagate rejection if any iteratee throws', async () => { + const errorFn = async (value: number, key: string) => { + if (value === 2) { + throw new Error('fail'); + } + return key; + }; + await expect(mapKeysAsync({ a: 1, b: 2, c: 3 }, errorFn)).rejects.toThrow('fail'); + }); + + it('should respect concurrency limit', async () => { + const obj = { a: 1, b: 2, c: 3, d: 4, e: 5 }; + + let running = 0; + let maxRunning = 0; + + const fn = vi.fn(async (value: number, key: string) => { + running++; + + if (running > maxRunning) { + maxRunning = running; + } + + await delay(20); + running--; + return key + value; + }); + + const concurrency = 2; + const result = await mapKeysAsync(obj, fn, { concurrency }); + + expect(result).toEqual({ a1: 1, b2: 2, c3: 3, d4: 4, e5: 5 }); + expect(maxRunning).toBeLessThanOrEqual(concurrency); + expect(fn).toHaveBeenCalledTimes(Object.keys(obj).length); + }); + + it('should use full concurrency when not specified', async () => { + const obj = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10 }; + + let running = 0; + let maxRunning = 0; + + const fn = async (value: number, key: string) => { + running++; + if (running > maxRunning) { + maxRunning = running; + } + await delay(20); + running--; + return key + value; + }; + + await mapKeysAsync(obj, fn); + expect(maxRunning).toBe(10); + }); +}); diff --git a/src/object/mapKeysAsync.ts b/src/object/mapKeysAsync.ts new file mode 100644 index 000000000..c14533f6f --- /dev/null +++ b/src/object/mapKeysAsync.ts @@ -0,0 +1,45 @@ +import { limitAsync } from '../array/limitAsync.ts'; + +interface MapKeysAsyncOptions { + concurrency?: number; +} + +/** + * Creates a new object with the same values as the given object, but with keys generated + * by running each own enumerable property of the object through the async iteratee function. + * + * @template T - The type of the object. + * @template K - The type of the new keys generated by the iteratee function. + * + * @param {T} object - The object to iterate over. + * @param {(value: T[keyof T], key: keyof T, object: T) => Promise} getNewKey - The async function invoked per own enumerable property. + * @param {MapKeysAsyncOptions} [options] Optional configuration object. + * @returns {Promise>} - A promise that resolves to the new mapped object. + * + * @example + * // Example usage: + * const obj = { a: 1, b: 2 }; + * const result = await mapKeysAsync(obj, async (value, key) => key + value); + * console.log(result); // { a1: 1, b2: 2 } + */ +export async function mapKeysAsync, K extends PropertyKey>( + object: T, + getNewKey: (value: T[keyof T], key: keyof T, object: T) => Promise, + options?: MapKeysAsyncOptions +): Promise> { + const result = {} as Record; + const keys = Object.keys(object) as Array; + + if (options?.concurrency) { + getNewKey = limitAsync(getNewKey, options.concurrency); + } + + await Promise.all( + keys.map(async key => { + const value = object[key]; + result[await getNewKey(value, key, object)] = value; + }) + ); + + return result; +} diff --git a/src/object/mapValuesAsync.spec.ts b/src/object/mapValuesAsync.spec.ts new file mode 100644 index 000000000..8a2839f22 --- /dev/null +++ b/src/object/mapValuesAsync.spec.ts @@ -0,0 +1,80 @@ +import { describe, expect, it, vi } from 'vitest'; +import { mapValuesAsync } from './mapValuesAsync.ts'; +import { delay } from '../promise/delay.ts'; + +describe('mapValuesAsync', () => { + it('should iterate over and map the object using its own values asynchronously', async () => { + const result = await mapValuesAsync({ a: 1, b: 2, c: 3 }, async (n: number) => n * 2); + expect(result).toEqual({ a: 2, b: 4, c: 6 }); + }); + + it('should pass the key corresponding to the current value into the iteratee', async () => { + const result = await mapValuesAsync({ a: 1, b: 2, c: 3 }, async (_, key) => key); + expect(result).toEqual({ a: 'a', b: 'b', c: 'c' }); + }); + + it('should pass the cloned object into the iteratee', async () => { + const result = await mapValuesAsync({ a: 1, b: 2, c: 3 }, async (value, key, object) => { + object[key] = value * 11; + return value * 11; + }); + + expect(result).toEqual({ a: 11, b: 22, c: 33 }); + }); + + it('should propagate rejection if any iteratee throws', async () => { + const errorFn = async (value: number) => { + if (value === 2) { + throw new Error('fail'); + } + return value; + }; + await expect(mapValuesAsync({ a: 1, b: 2, c: 3 }, errorFn)).rejects.toThrow('fail'); + }); + + it('should respect concurrency limit', async () => { + const obj = { a: 1, b: 2, c: 3, d: 4, e: 5 }; + + let running = 0; + let maxRunning = 0; + + const fn = vi.fn(async (value: number) => { + running++; + + if (running > maxRunning) { + maxRunning = running; + } + + await delay(20); + running--; + return value * 2; + }); + + const concurrency = 2; + const result = await mapValuesAsync(obj, fn, { concurrency }); + + expect(result).toEqual({ a: 2, b: 4, c: 6, d: 8, e: 10 }); + expect(maxRunning).toBeLessThanOrEqual(concurrency); + expect(fn).toHaveBeenCalledTimes(Object.keys(obj).length); + }); + + it('should use full concurrency when not specified', async () => { + const obj = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10 }; + + let running = 0; + let maxRunning = 0; + + const fn = async (value: number) => { + running++; + if (running > maxRunning) { + maxRunning = running; + } + await delay(20); + running--; + return value * 2; + }; + + await mapValuesAsync(obj, fn); + expect(maxRunning).toBe(10); + }); +}); diff --git a/src/object/mapValuesAsync.ts b/src/object/mapValuesAsync.ts new file mode 100644 index 000000000..1b406659f --- /dev/null +++ b/src/object/mapValuesAsync.ts @@ -0,0 +1,45 @@ +import { limitAsync } from '../array/limitAsync.ts'; + +interface MapValuesAsyncOptions { + concurrency?: number; +} + +/** + * Creates a new object with the same keys as the given object, but with values generated + * by running each own enumerable property of the object through the async iteratee function. + * + * @template T - The type of the object. + * @template K - The type of the keys in the object. + * @template V - The type of the new values generated by the async iteratee function. + * + * @param {T} object - The object to iterate over. + * @param {(value: T[K], key: K, object: T) => Promise} getNewValue - The async function invoked per own enumerable property. + * @param {MapValuesAsyncOptions} [options] Optional configuration object. + * @returns {Promise>} - A promise that resolves to the new mapped object. + * + * @example + * // Example usage: + * const obj = { a: 1, b: 2 }; + * const result = await mapValuesAsync(obj, async (value) => value * 2); + * console.log(result); // { a: 2, b: 4 } + */ +export async function mapValuesAsync( + object: T, + getNewValue: (value: T[K], key: K, object: T) => Promise, + options?: MapValuesAsyncOptions +): Promise> { + const result = {} as Record; + const keys = Object.keys(object) as K[]; + + if (options?.concurrency) { + getNewValue = limitAsync(getNewValue, options.concurrency); + } + + await Promise.all( + keys.map(async key => { + result[key] = await getNewValue(object[key], key, object); + }) + ); + + return result; +}