From e6accd870910f8df9487b6f54e1888bc5ae1ba92 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 12 Nov 2024 13:02:13 -0500 Subject: [PATCH] fix(all): be more lenient, reduce memory usage (#306) --- src/async/all.ts | 81 ++++++++++++++++++--------------------- tests/async/all.test-d.ts | 77 +++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 43 deletions(-) create mode 100644 tests/async/all.test-d.ts diff --git a/src/async/all.ts b/src/async/all.ts index b088f331..58df015f 100644 --- a/src/async/all.ts +++ b/src/async/all.ts @@ -1,13 +1,8 @@ import { AggregateError, isArray } from 'radashi' -type PromiseValues[]> = { - [K in keyof T]: T[K] extends Promise ? U : never -} - /** - * Functionally similar to `Promise.all` or `Promise.allSettled`. If - * any errors are thrown, all errors are gathered and thrown in an - * `AggregateError`. + * Wait for all promises to resolve. Errors from rejected promises are + * collected into an `AggregateError`. * * @see https://radashi.js.org/reference/async/all * @example @@ -20,18 +15,22 @@ type PromiseValues[]> = { * ``` * @version 12.1.0 */ -export async function all, ...Promise[]]>( - promises: T, -): Promise> +export async function all( + input: T, +): Promise<{ -readonly [I in keyof T]: Awaited }> -export async function all[]>( - promises: T, -): Promise> +export async function all( + input: T, +): Promise<{ -readonly [I in keyof T]: Awaited }> /** - * Functionally similar to `Promise.all` or `Promise.allSettled`. If - * any errors are thrown, all errors are gathered and thrown in an - * `AggregateError`. + * Check each property in the given object for a promise value. Wait + * for all promises to resolve. Errors from rejected promises are + * collected into an `AggregateError`. + * + * The returned promise will resolve with an object whose keys are + * identical to the keys of the input object. The values are the + * resolved values of the promises. * * @see https://radashi.js.org/reference/async/all * @example @@ -43,39 +42,35 @@ export async function all[]>( * }) * ``` */ -export async function all>>( - promises: T, -): Promise<{ [K in keyof T]: Awaited }> +export async function all>( + input: T, +): Promise<{ -readonly [K in keyof T]: Awaited }> export async function all( - promises: Record> | Promise[], + input: Record | readonly unknown[], ): Promise { - const entries = isArray(promises) - ? promises.map(p => [null, p] as const) - : Object.entries(promises) - - const results = await Promise.all( - entries.map(([key, value]) => - value - .then(result => ({ result, exc: null, key })) - .catch(exc => ({ result: null, exc, key })), - ), - ) + const errors: any[] = [] + const onError = (err: any) => { + errors.push(err) + } - const exceptions = results.filter(r => r.exc) - if (exceptions.length > 0) { - throw new AggregateError(exceptions.map(e => e.exc)) + let output: any + if (isArray(input)) { + output = await Promise.all( + input.map(value => Promise.resolve(value).catch(onError)), + ) + } else { + output = { ...input } + await Promise.all( + Object.keys(output).map(async key => { + output[key] = await Promise.resolve(output[key]).catch(onError) + }), + ) } - if (isArray(promises)) { - return results.map(r => r.result) + if (errors.length > 0) { + throw new AggregateError(errors) } - return results.reduce( - (acc, item) => { - acc[item.key!] = item.result - return acc - }, - {} as Record, - ) + return output } diff --git a/tests/async/all.test-d.ts b/tests/async/all.test-d.ts new file mode 100644 index 00000000..e762f699 --- /dev/null +++ b/tests/async/all.test-d.ts @@ -0,0 +1,77 @@ +import { all } from 'radashi' + +describe('all', () => { + test('array input', async () => { + const result = await all([] as Promise[]) + expectTypeOf(result).toEqualTypeOf() + }) + + test('object input', async () => { + const result = await all({} as Record>) + expectTypeOf(result).toEqualTypeOf>() + }) + + test('readonly array input of promises, promise-like objects, and non-promises', async () => { + const result = await all([ + Promise.resolve(1 as const), + new Thenable(2 as const), + 3, + ] as const) + + expectTypeOf(result).toEqualTypeOf<[1, 2, 3]>() + }) + + test('readonly array input with nested object', async () => { + const result = await all([{ a: 1 }, Promise.resolve({ b: 2 })]) + + expectTypeOf(result).toEqualTypeOf<[{ a: number }, { b: number }]>() + }) + + test('readonly object input of promises, promise-like objects, and non-promises', async () => { + const result = await all({ + a: Promise.resolve(1 as const), + b: new Thenable(2 as const), + c: 3, + } as const) + + expectTypeOf(result).toEqualTypeOf<{ + a: 1 + b: 2 + c: 3 + }>() + }) + + test('array input with nested promise', async () => { + const result = await all([[Promise.resolve(1 as const)] as const]) + + // Nested promises are not unwrapped. + expectTypeOf(result).toEqualTypeOf<[readonly [Promise<1>]]>() + }) + + test('object input with nested promise', async () => { + const result = await all({ + a: { b: Promise.resolve(1 as const) }, + }) + + // Nested promises are not unwrapped. + expectTypeOf(result).toEqualTypeOf<{ a: { b: Promise<1> } }>() + }) +}) + +class Thenable implements PromiseLike { + constructor(private value: T) {} + + // biome-ignore lint/suspicious/noThenProperty: + then( + onfulfilled?: + | ((value: T) => TResult1 | PromiseLike) + | undefined + | null, + onrejected?: + | ((reason: any) => TResult2 | PromiseLike) + | undefined + | null, + ): PromiseLike { + return Promise.resolve(this.value).then(onfulfilled, onrejected) + } +}