Skip to content

Commit

Permalink
feat: add all function to the async module (#305)
Browse files Browse the repository at this point in the history
  • Loading branch information
sodiray authored Jun 17, 2023
1 parent 77cb220 commit d5fc623
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 3 deletions.
24 changes: 23 additions & 1 deletion cdn/radash.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,28 @@ const parallel = async (limit, array, func) => {
}
return results.map((r) => r.result);
};
async function all(promises) {
const entries = isArray(promises) ? promises.map((p) => [null, p]) : 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 exceptions = results.filter((r) => r.exc);
if (exceptions.length > 0) {
throw new AggregateError(exceptions.map((e) => e.exc));
}
if (isArray(promises)) {
return results.map((r) => r.result);
}
return results.reduce(
(acc, item) => ({
...acc,
[item.key]: item.result
}),
{}
);
}
const retry = async (options, func) => {
const times = options?.times ?? 3;
const delay = options?.delay;
Expand Down Expand Up @@ -888,4 +910,4 @@ const trim = (str, charsToTrim = " ") => {
return str.replace(regex, "");
};

export { alphabetical, assign, boil, callable, camel, capitalize, chain, clone, cluster, compose, construct, counting, crush, dash, debounce, defer, diff, draw, first, flat, fork, get, group, guard, intersects, invert, isArray, isDate, isEmpty, isEqual, isFloat, isFunction, isInt, isNumber, isObject, isPrimitive, isString, isSymbol, iterate, keys, last, list, listify, lowerize, map, mapEntries, mapKeys, mapValues, max, memo, merge, min, objectify, omit, parallel, partial, partob, pascal, pick, proxied, random, range, reduce, replace, replaceOrAppend, retry, select, series, set, shake, shift, shuffle, sift, sleep, snake, sort, sum, template, throttle, title, toFloat, toInt, toggle, trim, tryit as try, tryit, uid, unique, upperize, zip, zipToObject };
export { all, alphabetical, assign, boil, callable, camel, capitalize, chain, clone, cluster, compose, construct, counting, crush, dash, debounce, defer, diff, draw, first, flat, fork, get, group, guard, intersects, invert, isArray, isDate, isEmpty, isEqual, isFloat, isFunction, isInt, isNumber, isObject, isPrimitive, isString, isSymbol, iterate, keys, last, list, listify, lowerize, map, mapEntries, mapKeys, mapValues, max, memo, merge, min, objectify, omit, parallel, partial, partob, pascal, pick, proxied, random, range, reduce, replace, replaceOrAppend, retry, select, series, set, shake, shift, shuffle, sift, sleep, snake, sort, sum, template, throttle, title, toFloat, toInt, toggle, trim, tryit as try, tryit, uid, unique, upperize, zip, zipToObject };
23 changes: 23 additions & 0 deletions cdn/radash.js
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,28 @@ var radash = (function (exports) {
}
return results.map((r) => r.result);
};
async function all(promises) {
const entries = isArray(promises) ? promises.map((p) => [null, p]) : 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 exceptions = results.filter((r) => r.exc);
if (exceptions.length > 0) {
throw new AggregateError(exceptions.map((e) => e.exc));
}
if (isArray(promises)) {
return results.map((r) => r.result);
}
return results.reduce(
(acc, item) => ({
...acc,
[item.key]: item.result
}),
{}
);
}
const retry = async (options, func) => {
const times = options?.times ?? 3;
const delay = options?.delay;
Expand Down Expand Up @@ -891,6 +913,7 @@ var radash = (function (exports) {
return str.replace(regex, "");
};

exports.all = all;
exports.alphabetical = alphabetical;
exports.assign = assign;
exports.boil = boil;
Expand Down
2 changes: 1 addition & 1 deletion cdn/radash.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "radash",
"version": "10.8.1",
"version": "10.9.0",
"description": "Functional utility library - modern, simple, typed, powerful",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.mjs",
Expand Down
70 changes: 70 additions & 0 deletions src/async.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fork, list, range, sort } from './array'
import { isArray } from './typed'

/**
* An async reduce function. Works like the
Expand Down Expand Up @@ -145,6 +146,75 @@ export const parallel = async <T, K>(
return results.map(r => r.result)
}

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.
*
* @example
* const [user] = await all({
* api.users.create(...),
* s3.buckets.create(...),
* slack.customerSuccessChannel.sendMessage(...)
* })
*/
export async function all<T extends Promise<any>[]>(
promises: T
): Promise<PromiseValues<T>>
/**
* Functionally similar to Promise.all or Promise.allSettled. If any
* errors are thrown, all errors are gathered and thrown in an
* AggregateError.
*
* @example
* const { user } = await all({
* user: api.users.create(...),
* bucket: s3.buckets.create(...),
* message: slack.customerSuccessChannel.sendMessage(...)
* })
*/
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, Promise<any>> | Promise<any>[]
>(promises: T) {
const entries = isArray(promises)
? promises.map(p => [null, p] as [null, Promise<any>])
: 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 exceptions = results.filter(r => r.exc)
if (exceptions.length > 0) {
throw new AggregateError(exceptions.map(e => e.exc))
}

if (isArray(promises)) {
return results.map(r => r.result) as T extends Promise<any>[]
? PromiseValues<T>
: unknown
}

return results.reduce(
(acc, item) => ({
...acc,
[item.key!]: item.result
}),
{} as { [K in keyof T]: Awaited<T[K]> }
)
}

/**
* Retries the given function the specified number
* of times.
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export {
zipToObject
} from './array'
export {
all,
defer,
guard,
map,
Expand Down
57 changes: 57 additions & 0 deletions src/tests/async.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,4 +485,61 @@ describe('async module', () => {
}
})
})

describe('_.all', () => {
const promise = {
pass: <T>(value: T) => new Promise<T>(res => res(value)),
fail: (err: any) => new Promise((res, rej) => rej(err))
}
it('returns array with values in correct order when given array', async () => {
const result = await _.all([
promise.pass(22),
promise.pass('hello'),
promise.pass({ name: 'ray' })
])
assert.deepEqual(result, [22, 'hello', { name: 'ray' }])
})
it('returns object with values in correct keys when given object', async () => {
const result = await _.all({
num: promise.pass(22),
str: promise.pass('hello'),
obj: promise.pass({ name: 'ray' })
})
assert.deepEqual(result, {
num: 22,
str: 'hello',
obj: { name: 'ray' }
})
})
it('throws aggregate error when a single promise fails (in object mode)', async () => {
try {
await _.all({
num: promise.pass(22),
str: promise.pass('hello'),
err: promise.fail(new Error('broken'))
})
} catch (e: any) {
const err = e as AggregateError
assert.equal(err.errors.length, 1)
assert.equal(err.errors[0].message, 'broken')
return
}
assert.fail('Expected error to be thrown but it was not')
})
it('throws aggregate error when a single promise fails (in array mode)', async () => {
try {
await _.all([
promise.pass(22),
promise.pass('hello'),
promise.fail(new Error('broken'))
])
} catch (e: any) {
const err = e as AggregateError
assert.equal(err.errors.length, 1)
assert.equal(err.errors[0].message, 'broken')
return
}
assert.fail('Expected error to be thrown but it was not')
})
})
})

1 comment on commit d5fc623

@vercel
Copy link

@vercel vercel bot commented on d5fc623 Jun 17, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.