sergiodxa/remix-utils

Would you be interested in this util function?

TomerAberbach opened this issue · 2 comments

Hey Sergio!

Thanks for this package. It's got some really nice utilities!

I recently had a need for the following function and was wondering if you'd be interested in a PR for adding it to the package:

export async function promiseAllWithDeterministicThrow<
  Values extends readonly unknown[] | [],
>(
  values: Values,
): Promise<{ -readonly [Index in keyof Values]: Awaited<Values[Index]> }> {
  const promises = values.map(value =>
    Promise.resolve(value)
      .then(resolved => ({ resolved }))
      .catch((error: unknown) => ({ error })),
  )

  const results: unknown[] = Array.from({ length: promises.length })

  for (let index = 0; index < promises.length; index++) {
    const result = await promises[index]

    if (`error` in result) {
      throw result.error
    }

    results[index] = result.resolved
  }

  return results as unknown as {
    -readonly [Index in keyof Values]: Awaited<Values[Index]>
  }
}

The case where I needed this is if I have multiple async authorization functions that may redirect by throwing a redirect response when the user is unauthorized, then a plain Promise.all call can result in nondeterministic redirects in the case where the user is unauthorized for multiple reasons. However, running the calls in series is not ideal either because it is slower.

So instead I was writing something like this if I had a pair of authorization calls to make:

const loader: LoaderFunction = ({ request }) => {
  const secondAuthorizationPromise = requireSecondThing(request)

  try {
    await requireFirstThing(request)
  } catch (e) {
    // No unhandled rejection in case this one throws too
    secondAuthorizationPromise.catch(() => {})

    throw e
  }

  await secondAuthorizationPromise
}

With this code the promise are still running concurrently, but I always respect the first promise's redirect or error before the second one regardless of which promise finishes first.

So the function I wrote in the beginning is a generalized version of this for N promises. Also note that this is a little different than Promise.allSettled because I don't actually have to wait for all of them to settle in all cases.

Let me know what you think!

Now that I think about it, I wonder if there's a function just like this in Remix itself. It seems like this same problem would come up when running loaders in parallel (which each might throw their own different redirects)