setTimeout in asynch fetch not working (javascript)

Viewed 46

I have a list of 16k or so objects I'm trying to fetch, no issues whatsoever fetching ONLY the id. But when I try to fetch the data for every id, my chrome can't handle it. I've looked through a dozen threads on here and whenever I try to add a new promise it either tells me it's invalid or that it only goes at the top or bottom. When I add a regular timeout (1 second) it just waits for example 1 second before fetching ALL 16k objects...

I'd appreciate if someone could help me solve this but also explain why it waits 1 second before going through everything at once INSTEAD of taking 1 second PER record.

async function fetchMODEL(endpoint) {
  var array;
  const whatever = await fetch("https://link.com/" + endpoint)
    .then((response) => response.json())
    .then((data) => (array = data));
  return whatever;
}

function detectIDs(){
  fetchMODEL("cars")
    .then((response) => {
      var data = response;
      var allIds = data.map(item => 
      {return item.id;})
      //console.log(JSON.stringify(allIds)) 
      //return allIds; 
      
      var illus = [];
      for(i=0; i < allIds.length ;i++){
        //setTimeout(
        fetchMODEL("cars/" + allIds[i])
          .then((response) => {
          //console.log(typeof response.illustrator)
          var artistData = {};
            if(typeof response.illustrator == "undefined"){
              artistData.edit="EDIT";
              artistData.name = response.name;
              artistData.id = response.id;
              illus.push(artistData);
              console.log(artistData)
            }else{            
            artistData.id = response.id;
            artistData.illustrator = response.illustrator;
            artistData.name = response.name;
            illus.push(artistData);
            console.log(artistData);
            }
            })//,100)
       }
    })
}
var orgSetIDs = detectIDs();

I should mention that my setTimeout is placed where it's placed for the simple reason that it wouldn't "run" anywhere else.

5 Answers

Setting a timeout is synchronous an immediate, in the sense that it registers the callback and continues without waiting, and then after 1 second, all of those timeouts trigger at the same time. Either way, using a hardcoded timeout to lower the load is not the best idea.


What I recommend is doing batched requests, you split all of your id's in groups of 20 or something, then make all of these requests and await all of them inside the loop, after they are done you continue with the next group.

function sliceIntoChunks(arr, chunkSize) {
    const res = [];
    for (let i = 0; i < arr.length; i += chunkSize) {
        const chunk = arr.slice(i, i + chunkSize);
        res.push(chunk);
    }
    return res;
}

const allIds = ...;
const batches = sliceIntoChunks(allIds, 20));

const results = []
for (const batch of batches) {
  // this will trigger all ids in batch to fetch concurrently
  const batchResults = await Promise.all(batch.map(id => load(id)))
  results.push(...batchResults);
}

// use results...

As a final suggestion, consider if you can request all of your resources in the same request instead, or use proper pagination if there's too much data.

Here's another way to batch requests

async function fetchIds() {
  // return fetch("https://link.com/cars").then(res => res.json());
  // for example, fill array with 1000 entries
  const test = [];
  for (let i = 0; i < 1000; ++i) {
    test.push(i);
  }
  return test;
}

async function fetchForId(id) {
  // return fetch("https://link.com/cars/" + id).then(res => res.json());
  // for example, return object
  return { id: id, test: "value" };
}

async function getData(){
  const ids = await fetchIds();
  const batchSize = 100; // can change this
  
  const allObjects = [];
  while (ids.length > 0) {
    const batch = ids.splice(0, batchSize); // remove up to batchSize elements
    // fetch all in the batch
    const objects = await Promise.all(batch.map(fetchForId));
    allObjects.push(...objects);
  }
  return allObjects;
}

getData().then(data => {
  console.log(JSON.stringify(data));
});

Here is another way to do it. Chrome has a limit of 13 parallel requests to the same host (if you start fetching more, Chrome will queue them) So we start by batching the first 13 requests. Then each time a request finishes, we start the next one in the queue.

With the pure batch approach, you will have to wait for all the requests of the batch to finish before starting the next batch. With an initial batch then a queue, most of the time you have 13 requests in parallel which is the max supported by Chrome.

Here is how it could be implemented:

async function fetchMODEL(endpoint) {
  var array;
  const whatever = await fetch("https://link.com/" + endpoint)
    .then((response) => response.json())
    .then((data) => (array = data));
  return whatever;
}

// Chrome can only make 13 parallel requests to the same host
const CHROME_MAX_PARALLEL_REQUEST = 13;

function detectIDs() {
  return fetchMODEL("cars").then(async (response) => {
    // We create a promise that will resolve when all cars data have been fetched
    let resolveFetchedAllCarsPromise;
    let fetchedAllCarsPromise = new Promise(
      (resolve) => (resolveFetchedAllCarsPromise = resolve)
    );

    var data = response;
    var allIds = data.map((item) => {
      return item.id;
    });

    // The nextIndex in the queue to be processed (this is set to CHROME_MAX_PARALLEL_REQUEST as we will batch CHROME_MAX_PARALLEL_REQUEST at the beginning)
    let nextIndex = CHROME_MAX_PARALLEL_REQUEST;
    // Keep track of the number of ids that are already processed
    let processedIds = 0;

    var illus = [];

    // fetchNext will be used to signal that a request has been processed and the next one in the queue should start
    const fetchNext = () => {
      processedIds++;
      // if all ids have been processed, we resolve the promise with the final values we have
      if (processedIds >= allIds.length) {
        resolveFetchedAllCarsPromise({ allIds, illus });
      }
      // if we already have started to process all car ids we just wait for the pending processing finish
      if (nextIndex >= allIds.length) return;
      // Start to fetch the next car data
      fetchCarData(allIds[nextIndex], illus, fetchNext);
      nextIndex++;
    };

    // Batch the first requests
    for (i = 0; i < CHROME_MAX_PARALLEL_REQUEST; i++) {
      fetchCarData(allIds[i], illus, fetchNext);
    }

    return fetchedAllCarsPromise;
  });
}

function fetchCarData(id, illus, fetchNext) {
  fetchMODEL("cars/" + id).then((response) => {
    //console.log(typeof response.illustrator)
    var artistData = {};
    if (typeof response.illustrator == "undefined") {
      artistData.edit = "EDIT";
      artistData.name = response.name;
      artistData.id = response.id;
      illus.push(artistData);
      console.log(artistData);
    } else {
      artistData.id = response.id;
      artistData.illustrator = response.illustrator;
      artistData.name = response.name;
      illus.push(artistData);
      console.log(artistData);
    }
    // here we signal that we have finished processing a request and want to fetch the next one in the queue
    fetchNext();
  });
}

async function getIdsAndIllus() {
  // here is an example to get allIds and illus
  // you need to wrap it in an async function to be able to use await
  const { allIds, illus } = await detectIDs();
  // do something with those
}

getIdsAndIllus();

try this

async function fetchMODEL(endpoint) {
  var array;
  const whatever = await fetch("https://link.com/" + endpoint)
    .then((response) => response.json())
    .then((data) => (array = data));
  return whatever;
}

async function detectIDs(){
  await fetchMODEL("cars")
    .then((response) => {
      var data = response;
      var allIds = data.map(item => 
      {return item.id;})
      //console.log(JSON.stringify(allIds)) 
      //return allIds; 
      
      var illus = [];
      for(i=0; i < allIds.length ;i++){
        //setTimeout(
        await fetchMODEL("cars/" + allIds[i])
          .then((response) => {
          //console.log(typeof response.illustrator)
          var artistData = {};
            if(typeof response.illustrator == "undefined"){
              artistData.edit="EDIT";
              artistData.name = response.name;
              artistData.id = response.id;
              illus.push(artistData);
              console.log(artistData)
            }else{            
            artistData.id = response.id;
            artistData.illustrator = response.illustrator;
            artistData.name = response.name;
            illus.push(artistData);
            console.log(artistData);
            }
            })//,100)
       }
    })
}
var orgSetIDs;
detectIDs().then((res) => {
    orgSetIDs = res;
});

The for loop you have is fetching all the cars at once because there is nothing to block the loop's execution. fetchMODEL returns a Promise, but does not block the thread; therefore, the for loop rapidly calls fetchMODEL for all ids.

Similarly, when you use setTimeout, the for loop runs through quickly because setTimeout is not blocking (it just schedules the callback for the future). After 100 milliseconds, the callback to each setTimeout will be executed.

If you wanted to fetch each car in the for loop, one after another, add the await keyword before your call to fetchMODEL. This will wait for the request to finish before allowing the for loop to advance to the next ID.

Related