Home > other >  Given list of API end points and it's priority, returns the API response with high priority
Given list of API end points and it's priority, returns the API response with high priority

Time:03-21

I got this question in an interview. A weather API endpoint can return the weather data for Pincode, city, state, and country.

Given a list of APIs and their priorities, call them parallelly and return the data for the API, which has high priority.

APIs = [
  { url: "api.weather.com/pin/213131", priority: 0 },
  { url: "api.weather.com/state/california", priority: 2 },
  { url: "api.weather.com/city/sanfrancisco", priority: 1 },
  { url: "api.weather.com/country/usa", priority: 3 },
];

In the above list, the priority order is pin > city > state > country. This means calling all the APIs parallelly; if the API with Pincode returned the data first, resolve the Promise immediately; if not, it should resolve the next higher priority one.

I thought of Promise.race(), but that won't consider a priority. Then I thought of waiting till a timeout occurs, and below is my code for the same. It simply waits till timeout, and if the high priority one is resolved first, then the real Promise will be resolved. After a specific timeout, it simply resolves with the first high priority response. But the interviewer wants me to implement it without a timeout.

Below is the code with a timeout. Does anyone know how to implement it without a timeout and more generic fashion?

function resolvePromiseWithPriority(APIS, timeout) {
  let PROMISES = APIS.sort((a, b) => a.priority - b.priority).map((api) => {
    return () =>
      new Promise((resolve, reject) => {
        fetch(api.url)
          .then((data) => resolve(data))
          .catch((err) => reject(err));
      });
  });

  let priorities = [...APIS.map((item) => item.priority)];
  let maxPriority = Math.min(...priorities);
  let minPriority = Math.max(...priorities);

  let results = [];
  let startTime = Date.now();

  return new Promise((resolve, reject) => {
    PROMISES.forEach((promise, priority) => {
      promise()
        .then((data) => {
          results[priority] = data;
          let gap = (Date.now() - startTime) / 1000;

          if (gap > timeout) {
            // resolve the current high priority promise, if no promises resolved before the timeout, resolve the first one resolved.
            // If all promises are rejected, reject this promise.

            if (!results[minPriority] instanceof Error)
              resolve(resolve[minPriority]);

            for (let item of results) {
              if (!item instanceof Error) resolve(item);
              reject("No promises resolved !!");
            }
          } else {
            if (priority === maxPriority) {
              // if the high priority promise gets it's data, resolve immediately.
              resolve(results[priority]);
            }

            if (priority < minPriority) {
              minPriority = priority;
            }
          }
        })
        .catch((err) => {
          results[priority] = err;
        });
    });
  });
}

CodePudding user response:

On the understanding that ...

Given a list of APIs and their priorities, call them parallelly and return the data for the API, which has high priority.

expands to ...

Given a list of APIs and their priorities, call them in parallel and return the data from the highest priority API that successfully delivers data; if no API is successful, then provide a custon Error.

then you neither want nor need a timeout, and the requirement can be met with three Array methods .sort(), .map() and .reduce() as follows:

function resolvePromiseWithPriority(APIS) {
    return APIS
        .sort((a, b) => a.priority - b.priority); // sort the APIS array into priority order.
        .map(api => fetch(api.url)) // initialte all fetches in parallel.
        .reduce((cumulativePromise, fetchPromise) => cumulativePromise.catch(() => fetchPromise); // build a .catch().catch().catch()... chain
    }, Promise.reject()) // Rejected promise as a starter for the catch chain.
    .catch(() => new Error('No successful fetches')); // Arrive here if none of the fetches is successful.
}

The .sort() and .map() methods are fully described by comments in the code.

The catch chain formed by the .reduce() method works as follows:

  • the starter Promise is immediately caught and allows the Pincode fetch a chance of providing the required data.
  • if fetch failure (Pincode or otherwise) gives the next highest priority fetch a chance of providing the required data, and so on, and so on, down the chain.
  • as soon as a successful fetch is encountered, the chain adopts its success path; all further catches in the chain are bypassed; with no thens anywhere in the chain, the promise returned by resolvePromiseWithPriority() delivers the successful fetch's data; any lower priority successes are effectively ignored as they would only ever be considered in the (bypassed) catches;
  • if all of the fetches fail, a terminal catch injects your custom 'No promises resolved' error.

CodePudding user response:

Here's what I came up with:

function resolvePromiseWithPriority(apis) {
    const promises = apis
        .sort((a, b) => a.priority - b.priority)
        .map(api => fetch(api.url).then(res => res.ok ? res.json() : Promise.reject(res.status));
    const contenders = [];
    let prev = null;
    for (const promise of promises) {
        prev = Promise.allSettled([
            prev ?? Promise.reject(),
            promise
        ]).then(([{status: prevStatus}])  => {
            if (prevStatus == 'rejected') return promise;
            // else ignore the promise
        });
        contenders.push(prev);
    }
    return Promise.any(contenders);
}

The contenders wait for all previous promises (those with a higher priority) and resolve (to the same result as the current promise) only once all those rejected. Then they're passed into Promise.any, which will go with the result of the first contender to fulfill, or an AggregateError of all request failures in order of priority.

Notice a contender fulfills to undefined if any of the previous promises did fulfill, but this doesn't really matter - Promise.any would go with the first one. The code would work also with Promise.allSettled([prev, promise]).then(() => promise), but imo checking prevStatus makes the intention a bit clearer.

The usage of Promise.allSettled (over prev.then(() => promise, () => promise)) and Promise.any ensures that the code causes no unhandled rejections.

  • Related