nicolas-van/modern-async

Could we add a `mapValues` implementation into the collection?

Opened this issue · 4 comments

Please explain the feature or improvement you would like:

I would love to see the addition of mapValues to the collection. This method would asynchronously transform the values of an object by applying the iteratee to each.

Please describe the use case where you would need that feature (the general situation or type of program where that would be helpful):

I often use records to organize my data, such as a record of user IDs paired with an access token:

const accessTokensByUserId: Record<Uuid, string> = {
    [Uuid(...)]: "token1",
    [Uuid(...)]: "token2",
}

.mapValues would be useful here if I'd like to make a request for each user using their relative access-key:

async function fetchBirthday(accessToken: string): Promise<Date> { ... }

const birthdayByUser: Record<Uuid, Date> = await async.mapValues(accessTokensByUserId, fetchBirthday)

This would be opposed to the manual implementation:

async function fetchBirthday(accessToken: string): Promise<Date> { ... }

const birthdaysByUser: Record<Uuid, Date> = Object.fromEntries(
    await async.map(Object.entries(birthdaysByUser), async ([uuid, token]) => {
        const birthday = await fetchBirthday(token)
        return [uuid, token]
    })
) as Record<Uuid, Date> // Needed since Object.fromEntries types its keys as `string` where before they were `Uuid`

If you know another library similar to this one that already propose that feature please provide a link:
Async's mapValues

Ok, I see the use case.

Actually it's relatively trivial to implement with something like this:

import { mapLimit, asyncWrap } from 'modern-async'

async function mapValuesLimit (obj, iteratee, queueOrConcurrency) {
  iteratee = asyncWrap(iteratee)
  return Object.fromEntries(await mapLimit(Object.entries(obj), async ([key, val]) => {
    const nval = await iteratee(val, key, obj)
    return [key, nval]
  }))
}

async function mapValuesSeries(obj, iteratee) {
  return mapValuesLimit(obj, iteratee, 1)
}

async function mapValues(obj, iteratee) {
  return mapValuesLimit(obj, iteratee, Number.POSITIVE_INFINITY)
}

I could consider adding it but I'm a bit reluctant. The problem is that modern-async does not actually provide any helpers regarding maps/objects. It only handles iterable/async iterables. If we wanted to provide helpers for object it would be necessary to think a lot more about it and consider adding a complete set of meaningful operations instead of just one (probably looking at libraries like lodash).

Yeah I was actually a bit surprised that there weren't methods for dealing with objects, but I can understand if the idea behind modern-async was to provide a solid foundation for async code and allow consumers to expand the toolset locally. If that is the intention though, the README phrasing might need to be updated to set the correct expectations:

Its goal is to be as complete as any of those libraries while being built from the very beginning with async/await and promises in mind.

(🤞 that doesn't come across as edgy)

No no, I think this is a good idea to expand the library in order to provide object-related helpers. It's just that there will be a need to find the time to do it. So it might come, but not soon. (Except if someone else want to do the job of identifying every useful functions that would be necessary, implementing them all and testing everything.)

I was also excited to find this library, as the TS types on caolan/async haven't been great.
But was sad when I realized a lot of the object related utils were missing.

In the meantime, I am using a little utils file that uses lodash (which I already have installed) and this library.
Very ugly and I'm sure there are some more clever ways to get the types to work better, but does the trick for now.

Posting here in case others find it useful

import { asyncMap, Queue } from 'modern-async';
import { toPairs, fromPairs } from 'lodash-es';

type RecordKey = string | number | symbol;

export async function asyncMapKeys<OK extends RecordKey, OV, MK extends RecordKey>(
  iterableObj: Record<OK, OV>,
  iteratee: (value: OV, key: OK) => Promise<MK> | MK,
  queueOrConcurrency?: Queue | number,
): Promise<Record<MK, OV>> {
  const objAsPairs: Array<[OK, OV]> = toPairs(iterableObj) as any;
  const mappedPairs = await asyncMap(objAsPairs, async ([key, value]) => {
    return [await iteratee(value, key), value] as [MK, OV];
  }, queueOrConcurrency);
  return fromPairs(mappedPairs) as Record<MK, OV>;
}

export async function asyncMapValues<OK extends RecordKey, OV, MV>(
  iterableObj: Record<OK, OV>,
  iteratee: (value: OV, key: OK) => Promise<MV> | MV,
  queueOrConcurrency?: Queue | number,
): Promise<Record<OK, MV>> {
  const objAsPairs: Array<[OK, OV]> = toPairs(iterableObj) as any;
  const mappedPairs = await asyncMap(objAsPairs, async ([key, value]) => {
    return [key, await iteratee(value, key)] as [OK, MV];
  }, queueOrConcurrency);
  return fromPairs(mappedPairs) as Record<OK, MV>;
}