0

By following a good answer by T.J. Crowder to a SO Thread, I managed to combine a loop of async tasks with Promise.all. The actual problem is, first I want to read one excel file in a Promisified function and a list of image files in the second Promisified function. Here is the code functions performing files reading.

import { User } from "./types";
import * as XLSX from "xlsx";


// Loading users data from Excel Data... Id,Name,CardNo
export async function loadUsersData(usersFile: File) {
  let result_users: User[] =await new Promise((resolve) => {
    var reader = new FileReader();
    reader.onload = function (e) {
      const data = e.target.result;
      const readedData = XLSX.read(data, { type: 'binary' });
      const wsname = readedData.SheetNames[0];
      const ws = readedData.Sheets[wsname];

      /* Convert array to json*/
      const parsedData = XLSX.utils.sheet_to_json(ws, { header: 1, blankrows: false });
      parsedData.shift();
      const users: User[] = parsedData.map((item: any) => {
        const id = item[0].toString().trim();
        const name = item[1].toString().trim();
        const cardNo = item[2].toString().trim();
        const user: User = { id, name, cardNo }; 
        return user;
      });

      resolve(users);
    }
    reader.readAsBinaryString(usersFile)

  });

  return result_users;
}


//Loading Images of Users Faces to display in material table along with other user info
export async function loadUsersFaces(users: User[], facesList: FileList) {
  const facesArray = Array.from(facesList)
  const promises=facesArray.map(async face=>{
    return await readFace(face, users);
  })
  let result_users: any=await Promise.all(promises);
  return result_users
}

function readFace(face: File,users:User[]) {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.onload = function (e) {
      let faceBase64String = e.target.result; //getting Base64String of image to render as custom column in material-table as https://material-table.com/#/docs/features/custom-column-rendering
      users.map(user => {
        if (face.name.includes(user.id) && face.name.includes(user.name)) {
          let newUser={ ...user, face: faceBase64String };
          console.log(`Resoling ${JSON.stringify(newUser)}`);
          resolve(newUser);
        }
      })
    }
    reader.readAsDataURL(face)
  });
}

And here is the code of Actions performing files reading one after the other.

//Here is usersFile is an excel file Blob and FileList contain list of image files
export const loadUsers = (usersFile: File,faces: FileList) => (dispatch:Dispatch) => {
  dispatch(actions.startCall({ callType: callTypes.list }));
  usersService.loadUsersData(usersFile).then((users:any)=>{  // Don't know how to tell compiler that it's User[]
    usersService.loadUsersFaces(users,faces).then((users:any)=>{
      console.log(users); // Here I should have users including Base64 Strings of face images in face property
      dispatch(actions.usersFetched({ totalCount:users.length, entities:users }));
    })
  })
};
DevLoverUmar
  • 11,809
  • 11
  • 68
  • 98
  • i don't think you should be using Promise.all in this scenario, as you are waiting for the promises to finish in a chain anyway – Krzysztof Krzeszewski Sep 11 '20 at 08:18
  • Sounds like you don't want to use `Promise.all`, as it will run al promises in parallel. Why not chain them? Each is dependent on the previous one, no? so promise1().then(promise2).then(promise3), etc – walidvb Sep 11 '20 at 08:18
  • Does this answer your question? [How do I access previous promise results in a .then() chain?](https://stackoverflow.com/questions/28250680/how-do-i-access-previous-promise-results-in-a-then-chain) – Harun Yilmaz Sep 11 '20 at 08:19
  • 1
    oh, you want to run the promises in series (one after the other), not in parallel (all at the same time)? Seems so, since you want the result of Promise 1 to be available to Promise 2 ... etc – Jaromanda X Sep 11 '20 at 08:19
  • what is `arrayofObjs` where you call `doSomeAsyncStuff(arrayofObjs)` - and `anArrayofObjs` in `resolve(anArrayofObjs);` - it's not clear – Jaromanda X Sep 11 '20 at 08:26
  • Hi all, I know about promise chaining but is there a simple way like Promise.all which runs synchronous ? I'm performing a file reading task in doSomeAsyncStuff. What if there are 1000 files? how to chain all the promises? – DevLoverUmar Sep 11 '20 at 09:08

3 Answers3

1

If you want to use loops to chain promises you will need async & await

async function chainPromiseNTimes(function_returning_promise,n,data){

  
    for(let i=0;i<n;i++) {

       data = await function_returning_promise(data)
       // data (modified) will be passed to new promise in next iteration
    }

    return data;
}

let result = await chainPromiseNTimes(doSomeAsyncStuff, 5, arrayofObjs)
ajay.16nk
  • 92
  • 3
1

You could try pushing functions returning promises instead of promises onto your array. This way you could simply call them when the data from the last promise is actually available.

function doSomeAsyncStuff(arrayofObjs) {
  // this is not rly asynchronous but for the purpose of example will do
  return new Promise(function(resolve) {
    const result = arrayofObjs.map(obj => ++obj);
    resolve(result);
  });
}

async function waitForPromiseChain(initialData, functionCallbacks) {
  let temp = initialData;
  for (let i = 0, l = functionCallbacks.length; i < l; i++)
    temp = await functionCallbacks[i](temp);
  return temp;
}

const promises = [];

for (i = 0; i < 5; i++) {
  promises.push((arrayofObjs) => doSomeAsyncStuff(arrayofObjs));
}

waitForPromiseChain([0, 0, 0, 0, 0], promises)
  .then(console.log);

In the example above I tried to keep code as close to your original as possible. However i took the liberty of redesigning function callbacks to accept any function in a chain instead of a single one.

If you are opposed to using async/await the same effect can be achieved with usage of normal then, even if with some difficulty.

function doSomeAsyncStuff(arrayofObjs) {
  // this is not rly asynchronous but for the purpose of example will do
  return new Promise(function(resolve) {
    const result = arrayofObjs.map(obj => ++obj);
    resolve(result);
  });
}

function waitForPromiseChain(initialData, functionCallbacks) {
  let temp = Promise.resolve(initialData);
  for (let i = 0, l = functionCallbacks.length; i < l; i++)
    temp = temp.then(data => functionCallbacks[i](data));
  return temp;
}

const promises = [];

for (i = 0; i < 5; i++) {
  promises.push((arrayofObjs) => doSomeAsyncStuff(arrayofObjs));
}

waitForPromiseChain([0, 0, 0, 0, 0], promises)
  .then(console.log);
Krzysztof Krzeszewski
  • 5,912
  • 2
  • 17
  • 30
1

My answer to this other question comes close to answering this, but I'm not sure it completely does.

Since you want to use the first operation's result in the second, and the second operation's result in the third, etc., you can't run the asynchronous actions in parallel. So you have to run them in series.

If you can use an async function (well supported these days), you'd do that like something this:

async function doSeriesOfThings() {
    let lastResult = /* the first value to pass, perhaps `undefined` or `null` */;
    for (const obj of arrayofObjs) {
        lastResult = await doSomeAsyncStuff(obj, lastResult);
    }

    return lastResult;
}

Live Example:

const arrayofObjs = [
    {value: 1},
    {value: 2},
    {value: 3},
];

function doSomeAsyncStuff(obj, value) {
    console.log(`doSomeAsyncStuff(${JSON.stringify(obj)}, ${value})`);
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(obj.value + value);
        }, Math.random() * 500);
    });
}

async function doSeriesOfThings() {
    let lastResult = 0;
    for (const obj of arrayofObjs) {
        lastResult = await doSomeAsyncStuff(obj, lastResult);
    }

    return lastResult;
}

doSeriesOfThings()
.then(result => console.log(`Final result: ${result}`))
.catch(error => console.error(`Error: ${error.message || String(error)}`));

If you also need an array of results, just build it up in the function:

async function doSeriesOfThings() {
    const results = [];
    let lastResult = /* the first value to pass, perhaps `undefined` or `null` */;
    for (const obj of arrayofObjs) {
        lastResult = await doSomeAsyncStuff(obj, lastResult)
        results.push(lastResult);
    }

    return results;
}

Live Example:

const arrayofObjs = [
    {value: 1},
    {value: 2},
    {value: 3},
];

function doSomeAsyncStuff(obj, value) {
    console.log(`doSomeAsyncStuff(${JSON.stringify(obj)}, ${value})`);
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(obj.value + value);
        }, Math.random() * 500);
    });
}

async function doSeriesOfThings() {
    const results = [];
    let lastResult = 0;
    for (const obj of arrayofObjs) {
        lastResult = await doSomeAsyncStuff(obj, lastResult)
        results.push(lastResult);
    }

    return results;
}

doSeriesOfThings()
.then(result => console.log(`Final result: ${JSON.stringify(result)}`))
.catch(error => console.error(`Error: ${error.message || String(error)}`));

If you can't use an async function, it's fairly similar, but you build up a promise chain:

function doSeriesOfThings() {
    let promise = Promise.resolve(/* the first value to pass, perhaps `undefined` or `null` */);
    for (const obj of arrayofObjs) {
        promise = promise.then(result => doSomeAsyncStuff(obj, result));
        // Or you can write it like this: `promise = promise.then(doSomeAsyncStuff);`
    }
    return promise;
}

Live Example:

const arrayofObjs = [
    {value: 1},
    {value: 2},
    {value: 3},
];

function doSomeAsyncStuff(obj, value) {
    console.log(`doSomeAsyncStuff(${JSON.stringify(obj)}, ${value})`);
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(obj.value + value);
        }, Math.random() * 500);
    });
}

function doSeriesOfThings() {
    let promise = Promise.resolve(0);
    for (const obj of arrayofObjs) {
        promise = promise.then(result => doSomeAsyncStuff(obj, result));
        // Or you can write it like this: `promise = promise.then(doSomeAsyncStuff);`
    }
    return promise;
}

doSeriesOfThings()
.then(result => console.log(`Final result: ${result}`))
.catch(error => console.error(`Error: ${error.message || String(error)}`));

And again, if you need an array of results, you can do that too:

function doSeriesOfThings() {
    const results = [];
    let promise = Promise.resolve(/* the first value to pass, perhaps `undefined` or `null` */);
    for (const obj of arrayofObjs) {
        promise = promise.then(result => doSomeAsyncStuff(obj, result).then(result => {
            results.push(result);
            return result;
        }));
        // Or you can write it like this: `promise = promise.then(doSomeAsyncStuff);`
    }
    return promise.then(() => results);
}

Live Example:

const arrayofObjs = [
    {value: 1},
    {value: 2},
    {value: 3},
];

function doSomeAsyncStuff(obj, value) {
    console.log(`doSomeAsyncStuff(${JSON.stringify(obj)}, ${value})`);
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(obj.value + value);
        }, Math.random() * 500);
    });
}

function doSeriesOfThings() {
    const results = [];
    let promise = Promise.resolve(0);
    for (const obj of arrayofObjs) {
        promise = promise.then(result => doSomeAsyncStuff(obj, result).then(result => {
            results.push(result);
            return result;
        }));
        // Or you can write it like this: `promise = promise.then(doSomeAsyncStuff);`
    }
    return promise.then(() => results);
}

doSeriesOfThings()
.then(result => console.log(`Final result: ${JSON.stringify(result)}`))
.catch(error => console.error(`Error: ${error.message || String(error)}`));
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Ok Thanks. What I'm doing in doAsyncStuff is something like new Promise((resolve) => {const reader = new FileReader(); reader.onload = .... ; reader.readAsDataURL(file)}} – DevLoverUmar Sep 11 '20 at 11:53
  • Can this file reading operation in a Promise cause any problem. At the moment My promise is resolved before the reading of all files completed... – DevLoverUmar Sep 11 '20 at 11:54
  • 1
    @DevLoverUmar - There's no reason you can't use a `FileReader`. You haven't shown how you're calling `resolve` but from what you've said I suspect you're running into [this problem](http://stackoverflow.com/questions/27643714/). You'd want: `return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = () => reject(reader.error); reader.readXYZ(); };` But in modern environments, use the methods on [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) instead, such as `text()` or `arrayBuffer()`. – T.J. Crowder Sep 11 '20 at 12:17
  • I updated the question with real code. If you have time please take a look. I'm very Sorry for inconvenience – DevLoverUmar Sep 11 '20 at 15:04