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 enginePredicate
- function to test if the signal should propagate to it's childrenModule
- intermediate object to housePlugins
to represent a website moduleImport
- es6 async module loaderPlugin
- 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.