Skip to content

Commit

Permalink
fix(all): be more lenient, reduce memory usage (#306)
Browse files Browse the repository at this point in the history
  • Loading branch information
aleclarson committed Nov 12, 2024
1 parent 6a49180 commit e6accd8
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 43 deletions.
81 changes: 38 additions & 43 deletions src/async/all.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import { AggregateError, isArray } from 'radashi'

type PromiseValues<T extends Promise<any>[]> = {
[K in keyof T]: T[K] extends Promise<infer U> ? 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
Expand All @@ -20,18 +15,22 @@ type PromiseValues<T extends Promise<any>[]> = {
* ```
* @version 12.1.0
*/
export async function all<T extends [Promise<any>, ...Promise<any>[]]>(
promises: T,
): Promise<PromiseValues<T>>
export async function all<T extends readonly [unknown, ...unknown[]]>(
input: T,
): Promise<{ -readonly [I in keyof T]: Awaited<T[I]> }>

export async function all<T extends Promise<any>[]>(
promises: T,
): Promise<PromiseValues<T>>
export async function all<T extends readonly unknown[]>(
input: T,
): Promise<{ -readonly [I in keyof T]: Awaited<T[I]> }>

/**
* 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
Expand All @@ -43,39 +42,35 @@ export async function all<T extends Promise<any>[]>(
* })
* ```
*/
export async function all<T extends Record<string, Promise<any>>>(
promises: T,
): Promise<{ [K in keyof T]: Awaited<T[K]> }>
export async function all<T extends Record<string, unknown>>(
input: T,
): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>

export async function all(
promises: Record<string, Promise<any>> | Promise<any>[],
input: Record<string, unknown> | readonly unknown[],
): Promise<any> {
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<string, any>,
)
return output
}
77 changes: 77 additions & 0 deletions tests/async/all.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { all } from 'radashi'

describe('all', () => {
test('array input', async () => {
const result = await all([] as Promise<number>[])
expectTypeOf(result).toEqualTypeOf<number[]>()
})

test('object input', async () => {
const result = await all({} as Record<string, Promise<number>>)
expectTypeOf(result).toEqualTypeOf<Record<string, number>>()
})

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<T> implements PromiseLike<T> {
constructor(private value: T) {}

// biome-ignore lint/suspicious/noThenProperty:
then<TResult1 = T, TResult2 = never>(
onfulfilled?:
| ((value: T) => TResult1 | PromiseLike<TResult1>)
| undefined
| null,
onrejected?:
| ((reason: any) => TResult2 | PromiseLike<TResult2>)
| undefined
| null,
): PromiseLike<TResult1 | TResult2> {
return Promise.resolve(this.value).then(onfulfilled, onrejected)
}
}

0 comments on commit e6accd8

Please sign in to comment.