2
\$\begingroup\$

I have created a little framework for my work to manage loading and unloading plugins by using a modular tree structure that is based on Promises.

I would like to be able to create the tree structure, but execute the async functions in order from the root node down, or more precisely execute the affects of the async function in order, the execution of the promises is not really important.

The way nested Promises work is the parent will not resolve until the child has first resolved eg.

let order = 0
const promiseTree = (name, children) => 
  Promise.all([
    new Promise(res => res(`${name} order:${order++}`)),
    children && Promise.all(children)
  ])

promiseTree('root', [
  promiseTree('child', [
    promiseTree('grandchild', [
      promiseTree('great grandchild')
    ])
  ])
])
.then(console.log)
<script src="https://codepen.io/synthet1c/pen/KyQQmL.js?concise=true"></script>

If you resolve a closure, then recursively call the callbacks once all promises are complete the order can be corrected.

let order = 0
const promiseTree = (name, children) => 
  Promise.all([
    new Promise(res => res(() => `${name} order:${order++}`)),
    children && Promise.all(children)
  ])
const recursivelyCall = x => 
  Array.isArray(x)
    ? x.map(recursivelyCall)
    : typeof(x) === 'function' ? x() : x

promiseTree('root', [
  promiseTree('child', [
    promiseTree('grandchild', [
      promiseTree('great grandchild')
    ])
  ])
])
// traverse the returned values and call the functions in declared order
.then(recursivelyCall)
.then(console.log)
<script src="https://codepen.io/synthet1c/pen/KyQQmL.js?concise=true"></script>

If I create a sample Promise tree it will execute from the inside out. see the first example below.

To achieve running the resolved promises in order, I have propagated a closure that contains the affects I want to perform up to the root Promise, which recursively calls the embedded closures in order that they were defined within the Promise tree.

For the framework I have made I would prefer not to have to return a closure as in increases the complexity for users.

Is there a better way to achieve the desired result? or should I continue with the second way?

The examples will show the order the promises are executed and the value they return. The second example is the same accept it recursively calls each closure in the response once it propagates to the root Promise.

First example

this shows the problem with the initial code. The child promises resolve before the parent promises

const trace = name => x => (console.log(name + ' promise'), x)

// resolve the test promise immediately
const testPromise = (id) => new Promise(res => {
  const time = Math.floor(Math.random() * 100)
  setTimeout(() => {
    res(`resolved promise ${id} after ${time} milliseconds`)
  }, time)
})

// first example
Promise.resolve(
  Promise.resolve(
    Promise.resolve(
      Promise.all([
        Promise.all([testPromise(1)]),
        Promise.all([testPromise(2), testPromise(3)]),
        // Promise.reject('something went wrong!')
      ]).then(trace('fourth'))
    ).then(trace('third'))
  ).then(trace('second'))
  .then(result => {
    console.log('resolved testPromise', result)
    return result
  })
  .catch(result => {
    console.error('failed testPromise')
    return result
  })
).then(trace('first'))
<script src="https://codepen.io/synthet1c/pen/KyQQmL.js?concise=true"></script>

Second example

This shows a possible solution of delaying the action using a closure to delay performing the action from the entire tree before the entire tree has resolved. This requires recursively calling each promise, then recursively running each returned closure once the entire tree has resolved successfully.

const trace = name => x => (console.log(name + ' promise'), x)

// resolve the test promise, but return a closure to delay the action 
// until all promises are resolved
const testPromiseReturnClosure = (id) => new Promise(res => {
  const time = Math.floor(Math.random() * 100)
  setTimeout(() => {
    res(() => `resolved closure ${id} after ${time} milliseconds`)
  }, time)
})

// flatMap over the returned values in the resolved promises values,
// if the value is a function call it.
const recursivelyCallResolvedClosures = x =>
  Array.isArray(x)
    ? x.forEach(recursivelyCallResolvedClosures)
    : typeof(x) === 'function'
      ? console.log(x())
      : null

Promise.resolve(
  Promise.resolve(
    Promise.resolve(
      Promise.all([
        Promise.all([testPromiseReturnClosure(1)]),
        Promise.all([testPromiseReturnClosure(2), testPromiseReturnClosure(3)]),
        // Promise.reject('something went wrong!')
      ]).then(trace('fourth'))
    ).then(trace('third'))
  ).then(trace('second'))
  .then(result => {
    console.log('resolved testPromiseReturnClosure', result)
    return result
  })
  .catch(result => {
    console.error('failed testPromiseReturnClosure')
    return result
  })
)
.then(trace('first'))
.then(x => (console.log('--------------------------------------'), x))
.then(recursivelyCallResolvedClosures)
<script src="https://codepen.io/synthet1c/pen/KyQQmL.js?concise=true"></script>

From replies below regarding the purpose of using a complex tree structure, here is a little example of the api that the framework uses.

The basic concept is that everything inherit's from a base class Node that provides init and destroy methods. The framework extends off this base class allowing each Node to perform some possibly asynchronous action then send a signal to it's children to do the same.

There are 5 different Node types that perform some function within the tree to allow you to simulate a router, lazy module loader, decision tree.

  • Core - the base engine
  • Predicate - function to test if the signal should propagate to it's children
  • Module - intermediate object to house Plugins to represent a website module
  • Import - es6 async module loader
  • Plugin - functionality to bind events and plugins to the current page

The tree will be run when the page loads, the internal state is updated or the page history is changed through history.pushState

The lifecycle is run init when the page is loaded, when another page is loaded, run the destroy method and pass the signal to each initialized Node. Once each node has removed it's functionality from the page, run the init cycle.

Basic api

class Node {
  nodes = []
  constructor(nodes = []) {
    this.nodes = nodes
  }
  init(request) {
    return Promise.all(this.nodes.map(node => node.init(request)))
  }
  destroy(request) {
    return Promise.all(this.nodes.map(node => node.destroy(request)))
  }
}
// example of extending the base Node class to do something then propagate the message to it's children conditionally.
class Predicate extends Node {
  constructor(predicate, nodes) {
    super(nodes)
    this.predicate = predicate
  }
  init(request) {
    // preform some action on init, then pass the message to any child `nodes`
    if (this.predicate(request))
      return super.init(request)
    return super.destroy(request)
  }
}

index.js

export default new Core([
  // Predicate could be related to the url, an element in the page, really anything that can be tested
  new Predicate(() => true, [
    // import asynchronously loads a module
    new Import(() => import('./somemodule'))
  ]),
  new Predicate(() => false, [
    new Plugin(/* this won't run because the predicate function is false */)
  ])
])

somemodule.js

export default new Module([
  // plugins initialize and destroy functionality on a page
  new Plugin(/* config options */)
])

It's kind of complex on the surface I guess, but simplifies the process of managing functionality for a website, and as it's a consistent api it's becomes very simple to manage functionality and state. But at the heart of it internally it's a Promise tree the same as the examples above.

Essentially I just want to fold the promises and perform them in their declared order. everything else works in terms of the tree.

\$\endgroup\$
1
  • \$\begingroup\$ Comments are not for extended discussion; this conversation has been moved to chat. \$\endgroup\$ Commented Jan 9, 2019 at 13:11

1 Answer 1

2
\$\begingroup\$

Nesting of the function calls

promiseTree('root', [
  promiseTree('child', [
    promiseTree('grandchild', [
      promiseTree('great grandchild')
    ])
  ])
])

is one issue with the code at the question relevant to the expected result. The innermost nested function (argument) is executed first. See and highlight line 15 at https://astexplorer.net/#/gist/777805a289e129cd29706b54268cfcfc/5a2def5def7d8ee91c052d9733bc7a37c63a6f67.

function promiseTree (name, children) {
  console.log(name, children, arguments[0], arguments); // arguments[0] is "great grandchild"
}

promiseTree('root', [
  promiseTree('child', [
    promiseTree('grandchild', [
      promiseTree('great grandchild')
    ])
  ])
])

If necessary, can link to a primary resource for verification of the fact that the innermost (argument) function is expected to be expected to be executed first in JavaScript.


The pattern at the question which uses multiple Promise.resolve() and Promise.all() is unnecessary.

Either recursion, Array.prototype.reduce() or async/await can be used to handle Promise execution in sequential order.

Note that Promise constructor or functions passed to Promise.all() do not necessarily execute in sequential order.

The array returned from Promise.all() will be in the same order as the elements in the iterable passed to Promise.all(), that is, for example, if the fifth element of the iterable is resolved before the first element of the iterable passed, the resulting array of values will still be in the exact order of indexes of the input iterable.

The logging of the times is not entirely accurate, as Promise.all() does not resolve until all elements of the iterable (whether the element is a Promise value or not a Promise) are resolved.

Given that Promise all is being used without Array.prototype.map() and using a reflect pattern, any rejected Promise will result in halting the Promise chain and .catch() being executed.

If there is a nested array structure, that array can be flattened (for example, using Array.prototype.flat()) before performing sequential operations.

const flatten  = arr => {
  arr = arr.flat();
  return arr.find(a => Array.isArray(a)) ? flatten(arr) : arr
}

flatten([1,[2, [3, [4]]]]) // [1, 2, 3, 4]

An an example of using both Array.prototype.reduce() and async/await to perform operations in sequential order, accepting a function, Promise or other value

const testPromise = (id) => new Promise((res, rej) => {
  const time = Math.floor(Math.random() * 1000)
  setTimeout(() => {
    console.log(`resolved promise '${id}' after ${time} milliseconds`);
    res(id)
  }, time)
});
const fn = (...arr) => {
  const res = [];
  return arr.reduce((promise, next) => {
      return promise.then(() => testPromise(next).then(data => {
        console.log(data);
        res.push(data)
      }).catch(err => err))
    }, Promise.resolve())
    .then(async() => {
      for (let value of res) {
        console.log(await (typeof value === 'function' ? value() : value))
      }
      return 'done'
    })
}

fn(1, () => 2, 3, Promise.resolve(4), testPromise(5)).then(data => console.log(data))

An approach using async/await

const fn = async() => {
  let order = 0
  const promiseTree = name =>
    new Promise(res => res(`${name} order:${order++}`))

  const res = [await promiseTree('root'), [
    await promiseTree('child'), [
      await promiseTree('grandchild'), [
        await promiseTree('great grandchild')
      ]
    ]
  ]];
  return res;
}

fn()
.then(console.log)

I would like to be able to create the tree structure, but execute the async functions in order from the root node down, or more precisely execute the affects of the async function in order, the execution of the promises is not really important.

To 1) execute N asynchronous operations in parallel (return a Promise object) which resolves to a value 2) create the data structure in order of input

// do asynchronous stuff, resolve `props`: Array of `[index, value]`
const fn = (...props) => 
  new Promise(resolve => 
    setTimeout(resolve, Math.floor(Math.random() * 1000), props));

// accepts string of characters delimited by space or array
// returns Array
const promiseTree = tree => 
  Promise.all(
    // check if `tree` is String or Array
    [...(!Array.isArray(tree) && typeof tree === 'string' 
      ? tree.split` ` 
      : tree).entries()] // use `.entries()` method of Array
    .map(([index, prop]) => fn(index, prop))) // do asynchronous stuff
  .then((result, res = [], t = []) => // define `res`, `t` Arrays
    result.map(([index, prop]) => 
      !res.length // first iteration
      ? res.push(prop, t) // push first value
      : index < result.length-1 // check index
        ? t.push(prop, t = []) // `.push()` to `t`, re-declare `t` as `[]`
        : t.push(prop)) // `.push()` last value `prop` to `t`
    && res) // return `res`

Promise.all(
[
  // space delimited String passed to `promiseTree`
  promiseTree('root child grandchild greatgrandchild') 
  .then(console.log)
  // Array passed to `promiseTree`
, promiseTree([...Array(4).keys()])
  .then(console.log)
]);

\$\endgroup\$
12
  • \$\begingroup\$ Thanks for the example. This is not the problem I am trying to solve as your Promises are not nested in a tree structure they are flat and your are synchronously resolving the promises. I want them parallel, but to have the parents effect to update the page before the child. I have added two more basic examples that should outline the problem and potential solution more clearly. I need to get some sleep as I start work in a couple of hours, but I'll try to get back to you if you reply. Cheers for your help. \$\endgroup\$
    – synthet1c
    Commented Jan 7, 2019 at 19:16
  • \$\begingroup\$ @synthet1c "and your are synchronously resolving the promises" ? Flatten any "tree" structure before performing any further tasks. "I want them parallel" The question specifically states that the requirement is to perform potentially asynchronous operations in a sequential order, not in parallel. \$\endgroup\$ Commented Jan 7, 2019 at 19:18
  • \$\begingroup\$ I will update. I mean to say execute the parent before executing the child. Eg the parent may be an ajax request to get some html from the server, the child may be some jQuery plugin to attach to the html from the parent. At the sibling level it doesn't matter the order, but parent to child must be consecutive. \$\endgroup\$
    – synthet1c
    Commented Jan 7, 2019 at 19:20
  • \$\begingroup\$ @synthet1c That is precisely what the code at the answer achieves. \$\endgroup\$ Commented Jan 7, 2019 at 19:21
  • 1
    \$\begingroup\$ lol, cheers for the warning. \$\endgroup\$
    – synthet1c
    Commented Jan 7, 2019 at 19:47

Not the answer you're looking for? Browse other questions tagged or ask your own question.