2

I need to get the result of a Javascript Promise that returns the fastest, but I want to continue invoking the logic encapsulated within the other 2 "losing" promises, irrespective of who wins. Example below.

// The 3 promises I care about
const fetchFromGoogle: Promise<T> = googlePromise()
const fetchFromAmazon: Promise<T> = amazonPromise()
const fetchFromCloudflare: Promise<T> = cloudflarePromise()

// The promise that invoked its logic the fastest
const winner: T = Promise.race([fetchFromGoogle, fetchFromAmazon, fetchFromCloudflare])

In this scenario, if fetchFromAmazon call wins in terms of speed, then I would return the result to the client, but continue running the other two promises async.

This is being executed from within a Cloudflare Worker and the ability to return the winning promise whilst continuing the evaluation of the other functions will be supported via the waitUntil API linked below.

I've evaluated two options:

  1. Some Javascript API that I'm unaware of that can do this for me
  2. Use something like this to determine which promises lost and run them using Cloudflare Workers context.waitUntil call which will ensure that the logic will continue evaluating despite having returned the result back to the client.

It's in my understanding Promise.All would not satisfy this criteria because I would never early return the winning promise as we wait for all 3 to complete.

user38643
  • 341
  • 1
  • 7
  • Take a look at the source of `Promise.race`. It's very easy to implement, and not some kind of magic API that only browsers can provide. It just loops through your array and calls `then()` on each promise. That's where you should start. – Evert Nov 05 '22 at 22:32
  • @Evert Any opinions on my proposed solution underneath (2). I assume that's probably the best in my eyes as it implies I kind of pick apart `Promise.race` – user38643 Nov 05 '22 at 22:39

1 Answers1

3

You can just combine a .then() handler with Promise.race(). The .then() handler lets you handle each result individually. The Promise.race() tells you when the first one is done (and optionally what its value was):

// The 3 promises I care about
const fetchFromGoogle: Promise<T> = googlePromise().then(result => { 
    /* process this one here whenever it completes */
    return someValue;
});
const fetchFromAmazon: Promise<T> = amazonPromise().then(result => { 
    /* process this one here whenever it completes */
    return someValue;
});
const fetchFromCloudflare: Promise<T> = cloudflarePromise().then(result => { 
    /* process this one here whenever it completes */
    return someValue;
});

// Tells you when the first one is done and what its value was
const winner: T = await Promise.race([fetchFromGoogle, fetchFromAmazon, fetchFromCloudflare]);

Keep in mind that Promise.race() tracks the first promise to settle (success or failure) - it does not give you the first promise to fulfill. So, if the first promise to settle rejects, then that's what Promise.race() will give you (the rejected promise).

You can use Promise.any() instead to get the first fulfilled promise.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Thanks, I didn't realize until after that the promises that don't win still continue execution, which is good to know. Any suggestions on incorporating `context.waitUntil` into this design? Because this is an HTTP handler, I need to ensure the other two promises continue execution after returning to the client. While your solution allows manipulation of the reponse, there is not a guarantee that they will continue after returning from the handler. – user38643 Nov 06 '22 at 00:53
  • btw, thanks for clarifying `race` vs `any`. I forgot about that distinction, which would cause a bit of trouble for me! – user38643 Nov 06 '22 at 00:56
  • 1
    @user38643 - I don't really know the environment that needs `context.waitUntil`, but you could do a separate `Promise.allSettled([fetchFromGoogle, fetchFromAmazon, fetchFromCloudflare])` with `context.waitUntil` if you somehow need to tell it to keep the request going until all the promises have settled. – jfriend00 Nov 06 '22 at 01:26
  • Thanks for the reply. I'm not sure that would satisfy this requirement as the resultant promise wouldn't be fulfilled until they all complete. Maybe the best/only solution is a promise inside ctx.waitUntil that is a sleep timer. – user38643 Nov 06 '22 at 05:56
  • 2
    In JavaScript you can use the same promise in multiple places, so you can do both `waitUntil(Promise.allSettled([a, b])` _and_ `winner = await Promise.race([a, b])`. – Kenton Varda Nov 06 '22 at 16:40
  • @user38643 - As Kenton mentioned, you can use the same promises in two separate operations. Use the `Promise.race()` or `Promise.any()` to tell you which one completes first and use the `Promise.allSettled()` with `waitUntil` to keep your request handler alive even after you've sent the response. – jfriend00 Nov 06 '22 at 17:15
  • To clarify, this won't invoke the same function multiple times, right? In Kenton's example, if I race [a,b] and then wrap [a,b] in promise.allSettled, I won't reinvoke a or b, correct? – user38643 Nov 06 '22 at 19:16
  • 1
    @user38643 - Correct. `a` and `b` are just promises. They don't run anything. They're just a notification system. The underlying operation was run once and created the promise. You can then use that promise for multiple things. – jfriend00 Nov 06 '22 at 19:24
  • Awesome, thanks for taking the time to explain all of this. Hope you enjoy your day – user38643 Nov 06 '22 at 19:54