Skip to main content

Resolving JavaScript Promises subsequently

Submitted by kaa4ever on Wed, 08/05/2015 - 09:08

The use case

I’m working on a Drupal 8 project, where we build nodes in the frontend and at some point wants to save them. The challenge is that these nodes can have an unknown number of references to other nodes.
In Drupal8, to save a node with references, you need to know the UUID of the referenced node, before saving the main node itself.

The following is the JavaScript abstraction of a Drupal node we will use as example:

let network = {
  'field_description': 'Friends and family',
  'field_creator': [
   {
     'field_name': 'Jack Johnson'
    }
  ],
  'field_network': [
    {
      'field_fullname': 'John Smith'
    },
    {
      'field_name': 'Peter Smith'
    }
  ]
};

So this a model of a simple network. We have a description of the network (field_description), the person creating the network (field_creator) and an array of people in the creators network (field_network). The description field is just a plain field. The creator field will be a reference to some sort of person node, as is the network, though the network field can have multiple references.

The obvious solution: Promise.all()

Using the Promise.all() function we could make each reference a promise and wait for them all to resolve before saving the network node itself. This could look something like this:

let save = () => {
  let promises = [];
  // Save all references first.
  for (let property in network) {
    if (network.hasOwnProperty(property) && Array.isArray(network[property])) {
      network[property].forEach((reference, index) => {
        promises.push(executeSave(network[property], property, index));
      });
    }
  }
 
  Promise.all(promises).then(values => {
    values.forEach(reference => {
      network[reference.field][reference.index] = reference.value;
    });
    return executeSave(network);
  });
};
 
let executeSave = (reference, property, index) => {
  return new Promise((resolve, reject) => {
    // Mock a HTTP call by setting a 1sec timeout.
    setTimeout(() => {
      // Get a UUID of the new node.
      let id = Math.floor(Math.random() * 100) + 1
      resolve({field: property, index: index, value: id})
    }, 1000);
  });
};

The problem

While this might actually work in some cases, I experienced a major problem. The loop in line 4 was executing within milliseconds intervals, which made the backend query the database too fast and therefore ending up with one or more deadlocks.
A deadlock is never good, and in the end, it would make some of the promises fail. Failure is equally bad to deadlocks and the result was the network itself not being saved.

Another thing to notice in this solution. The promise from the executeSave() method, must resolve with an object with information about field, index and value. In the solution we will se next, the promise will just resolve with the value (reference ID), which is in all cases, a more neat solution.

The solution: Promise sequence

After thinking about how to solve this problem, I realised that I had to make each invocation of executeSave() wait for the previous call to resolve, before querying the backend.

How to achieve this? Here is my solution:

let save = () => {
  // This promise will resolve immediately.
  let promise = Promise.resolve();
    // Save the references one by one.
    for (let property in network) {
      if (network.hasOwnProperty(property) && Array.isArray(network[property])) {
        network[property].forEach((reference, index) => {
          promise = promise.then(function() {
            return executeSave(reference).then(id => {
              network[property][index] = id;
            });
          });
        });
      }
    }
    // Now save the network itself.
    return promise.then(function() {
      return executeSave(network);
    });
  };
 
  let executeSave = reference => {
    return new Promise((resolve, reject) => {
    // Mock a HTTP call by setting a 1sec timeout.
    setTimeout(() => {
      // Get a ID of the new node.
      let id = Math.floor(Math.random() * 100) + 1
      resolve(id)
    }, 1000);
  });
};

The “magic” happens in line 8. This line updates the promise variable, to the result of a new Promise, but the invocation of then() on the existing promise, makes sure the actual call to the executeSave() method is not done until the previous promise has resolved.
And that my friend, is no less than brilliant.

Conclusion

The first time I heard about Promises, I didn’t really get the fuzz. What where they trying to solve? Callbacks seemed to do the same, and ending up with a promise pyramid instead of a callback pyramid, didn’t seem worth the change.
But that was of course me, not grasping and understanding Promises. They are great! And they are here to stay.
Promise.all() might never give you any problems like I had, but if it ever does, now you know that once again Promises will save the day.

Comment? Tweet me