The proper ways to stack optioned promises

Viewed 102

What would be the proper or the best way to collect all data from DB with promises, but with using native Node promises.

The goal is only to present what is selected:

const allPromises = [];
const selected = {
  sectionA: true,
  sectionB: false,
  sectionCIds: [ 1, 2, 4 ],
};

if (selected.sectionA) {
  allPromises.push(getSectionADataFromDbPromise());
}
if (selected.sectionB) {
  allPromises.push(getSectionBDataFromDbPromise());
}
if (selected.sectionCIds.length > 0) {
  allPromises.push(selected.sectionCIds
    .map(getSectionCDataFromDbPromise)
  );
}
Promise.all(allPromises)
  .then((allResults) => {
    if (selected.sectionA) {
      dataA = allResults[0];
    }
    if (selected.sectionA) {
      dataB = allResults[1];
    }
    if (selected.sectionC) {
      dataC = allResults[2]; // <-- this will not work if B is not selected
    }

    // ... same logic to build report: return Promise.all()...
  });

Possible solutions:

  • Track index for each data selected (eg. index of C will be 1)
  • Object Map
  • Add else { allPromises.push(Promise.resolve(null)) } to every if

Is there maybe an easier or one of this will be the proper way?

3 Answers

What do you think about this ? It's bigger, it's heavier, it's more difficult, but it's all automatized and fully evolutive. Wanna handle a new parameter ? A parameter have data now ? Change the map only.


I create a map that would contains everything we need to use a loop. The state of the data (activated or not), the function to call to get the data and so on.

const mapSelected = {
  sectionA: {
    state: true,

    func: getSectionADataFromDbPromise,
  },

  sectionB: {
    state: false,

    func: getSectionBDataFromDbPromise,
  },

  sectionC: {
    state: true,

    func: getSectionCDataFromDbPromise,

    data: [
      1,
      2,
      4,
    ],
  },
};

Then we create the promise array using the map we has created. Handling the case with data and without data.

const promises = Object.values(mapSelected).reduce((tmp, {
  state,
  func,
  data,
}) => {
  if (!state) return tmp;

  if (data && data.length) {
    return [
      ...tmp,

      ...data.map(x => func.call(this, x)),
    ];
  }

  return [
    ...tmp,

    func.call(this),
  ];
});

Then we create arrays from the promise return for each key on the map. You can change how I present the data, I didn't knew what you really wanted there.

Promise.all(promises)
  .then((allResults) => {
    let i = 0;

    const [
      dataA,
      dataB,
      dataC,
    ] = Object.values(mapSelected).reduce((tmp, {
      state,
      data,
    }, xi) => {
      if (!state) return tmp;

      if (data && data.length) {
        data.forEach(x => (tmp[xi].push(allPromises[i++])));

        return tmp;
      }

      tmp[xi].push(allPromises[i++]);

      return tmp;
    }, Object.values(mapSelected).map(() => []));
  });


@EDIT

I just did a snippet about the code I've made, run it

function a() {
  return 1;
}

const mapSelected = {
  sectionA: {
    state: true,

    func: a,
  },

  sectionB: {
    state: false,

    func: a,
  },

  sectionC: {
    state: true,

    func: a,

    data: [
      1,
      2,
      4,
    ],
  },
};

const allPromises = [
  0,
  1,
  2,
  3,
  4,
];

let i = 0;

const [
  dataA,
  dataB,
  dataC,
] = Object.values(mapSelected).reduce((tmp, {
  state,
  data,
}, xi) => {
  if (!state) return tmp;

  if (data && data.length) {
    data.forEach(x => (tmp[xi].push(allPromises[i++])));

    return tmp;
  }

  tmp[xi].push(allPromises[i++]);

  return tmp;
}, Object.values(mapSelected).map(() => []));

console.log(dataA);
console.log(dataB);
console.log(dataC);

Don't use push on the arrays conditionally, but always put the same value at the same index. Even if the value is nothing - Promise.all will handle that just fine.

const selected = {
  sectionA: true,
  sectionB: false,
  sectionCIds: [ 1, 2, 4 ],
};
Promise.all([
  selected.sectionA ? getSectionADataFromDbPromise() : null,
  selected.sectionB ? getSectionBDataFromDbPromise() : null,
  Promise.all(selected.sectionCIds.map(getSectionCDataFromDbPromise))
]).then(([dataA, dataB, dataC]) => {
  if (selected.sectionA) {
    // use dataA
  }
  if (selected.sectionA) {
    // use dataB
  }
  if (dataC.length) { // same as selected.selectionCIds.length
    // use dataC
  }
});

Unfortunately, unlike libraries such as Q, the standard Promise does not expose a variant of all taking an object of promises.

However, we can use the new ES2015 and ES2017 Object utility methods to assist us in keeping the code readable.

const allPromises = {};
const selected = {
  sectionA: true,
  sectionB: false,
  sectionCIds: [1, 2, 4],
};

if (selected.sectionA) {
  allPromises.sectionA = getSectionADataFromDbPromise();
}
if (selected.sectionB) {
  allPromises.sectionB = getSectionBDataFromDbPromise();
}
if (selected.sectionBIds.length > 0) {
  allPromises.sectionC = Promise.all(selected.sectionBIds
    .map(getSectionCDataFromDbPromise)
  );
}

Now we can write

Promise.all(Object.entries(allPromises).map(([key, promise]) =>
    promise.then(value => ({[key]: value}))
  ))
  .then(allResults => {
    const results = Object.assign({}, ...allResults);

    const data = {
      a: results.sectionA,
      b: results.sectionB,
      c: results.sectionB && results.sectionC
    };

    // ... same logic to build report: return Promise.all()...
  });
Related