0

I have the following program (it should be executable as such) in which I have problems to handle a recursive call to a method that involves many asynchronous operations.

As a starting point in the program I have a test() method. It triggers the first call to _add() to which it passes the root object (a tree of nested "elements").

The _add method is responsible for adding the nested elements into the storage (which I have represented here by a simple array). In order to do so, I have written the function so that it calls itself on all the children elements.

Storing an element is an asynchronous operation. Hence, I am using an array of promises to hold the "not yet available" value of all children elements.

As show by test(), I do not manage to properly handle the synchronization here. All elements should have been inserted in the storage before the promise returned by the first call to add() resolves. This it apparently not the case as the output of the program shows.

Of course, since the parentId of a child refers to the id of its parent, it's important for me that the storage operation happens in the same order at which the tree of elements is explored.

I am anticipating to have the exact same kind of problem when trying to retrieve elements from the storage.

Any help would be greatly appreciated.

Thanks in advance.

(() => {
  const storage = [];
  const root = {
    _type: "root",
    title: "root element",
    gravity: null,
    children: [
      {
        _type: "space",
        title: "my first space",
        gravity: 0,
        children: [
          {
            _type: "bundle",
            title: null,
            gravity: 0,
            children: [
              {
                _type: "vis",
                title: "my first vis",
                gravity: 0,
                visualization: {
                  url: "http://dummyvis.com/#8766"
                },
                children: null
              },
              {
                _type: "feature",
                title: "my feature 1",
                gravity: 1,
                feature: {
                  componentType: "CPT_FEATURE1"
                },
                children: []
              },
              {
                _type: "feature",
                title: "my feature 2",
                gravity: 1,
                feature: {
                  componentType: "CPT_FEATURE2"
                },
                children: []
              }
            ]
          }
        ]
      }
    ]
  };

  const store = (element, parentId) => {
    return new Promise( (resolve, reject) => {

      storage.push({id:element.id, _type:element._type, gravity: element.gravity, parent:parentId, created_date:Date.now() });

      setTimeout( () => resolve(element), 500);
    });
  }

  const _add = (element, recurse, parentId) => {
    console.log(element._type);
    if(!element._type) 
      throw new Error("an element must declare a key '_type'");
    parentId = parentId || null;

    return new Promise( (resolve, reject) => {
      const promises = [];

      promises.push( new Promise( (resolve, reject) => {
        store(element, parentId)
          .then(element => {
            resolve(element);
          })
          .catch(err => reject(err));
      }));

      Promise.all(promises)
        .then( res => {
          if(recurse && element.children.length > 0) {
            element.children.forEach( child => {
              _add(child, recurse, element.id);
            });
          }
          resolve(element);
        })
        .catch( err => reject(err));
    });
  }

  const test = () => {
    _add(root, true)
      .then( res => {
        console.log("----------------------------------------------------------");
        console.log("Test completed. Nothing should be printed after this line!");
        console.log("------------------------------------------------------...-");
      })
      .catch( err => {

      });
  }
  test();
})();

output:

decisionspace
----------------------------------------------------------
Test completed. Nothing should be printed after this line!
------------------------------------------------------...-
bundle
visualization
feature
feature
Mikou
  • 579
  • 1
  • 8
  • 17
  • Avoid the [`Promise` constructor antipattern](http://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it)! – Bergi Jan 19 '17 at 18:09

1 Answers1

0

The issue is here:

  Promise.all(promises)
    .then( res => {
      if(recurse && element.children.length > 0) {
        element.children.forEach( child => {
          _add(child, recurse, element.id);
        });
      }
      resolve(element);
    })

_add() returns a promise immediately and will complete its task later, asynchronously. When the forEach loop is done, the _add()s have not yet completed their task.

If you want to ensure that elements are added in order, waiting that the previous _add() has completed is task before invoking the next one, you can write:

  Promise.all(promises)
    .then( res => {
      var taskQueue = Promise.resolve();
      if(recurse && element.children.length > 0) {
        element.children.forEach( child => {
          taskQueue = taskQueue.then(() => _add(child, recurse, element.id));
        });
      }
      taskQueue.then(() => resolve(element));
    })

If you don't care if the elements are not added in order, you can write:

  Promise.all(promises)
    .then( res => {
      var tasks = [];
      if(recurse && element.children.length > 0) {
        element.children.forEach( child => {
          tasks.push(_add(child, recurse, element.id));
        });
      }
      Promise.all(tasks).then(() => resolve(element));
    })
Claude
  • 86
  • 4