Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(all): be more lenient, reduce memory usage #306

Merged
merged 2 commits into from
Nov 12, 2024
Merged
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
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)
}
}
Loading