Run multiple recursive Promises and break when requested
I'm working on a LED strip animation tool which allows the user to select multiple effects which can run simultaneously. Each effect is a (bluebird) Promise. There is a single run()
method which sets the color of the LED strip.
All promises run at a fixed FPS using the delay
method.
run(mode) {
return this.setStripColor(this.color).delay(1 / this.fps).then(() => { this.run(1 / this.fps) })
}
// example of an effect
rainbowSweep() {
// ..
// magical unicorn code
// ..
return Promise.resolve().delay(1 / this.fps).then(() => {
this.rainbowSweep()
})
app.rainbowSweep()
app.run()
Is there some sort of data structure that I can use where I can toggle on and off a recursive promise? In other words, how do I signal to the effect (the recursive promise) to stop recursing?
I was thinking of an array containing all the promises.
But then I don't know how to break/resolve a recursive promise when it's no longer in the array. I could do a check before I return
whether the promise itself is inside the array, but I was hoping there was a more elegant way.
Let's look at a simple recursive function that expresses our program at a high level
let RUNNING =
true
const main = async (elem, color = Color ()) =>
RUNNING
? delay (color, FPS)
.then (effect (color => setElemColor (elem, color)))
.then (color => main (elem, stepColor (color)))
: color
We've done a little wishful thinking with Color
, stepColor
, and setElemColor
(among others), let's implement those first
const Color = (r = 128, g = 128, b = 128) =>
({ r, g, b })
const stepColor = ({ r, g, b }, step = 8) =>
b < 255
? Color (r, g, b + step)
: g < 255
? Color (r, g + step, 0)
: r < 255
? Color (r + step, 0, 0)
: Color (0, 0, 0)
const setElemColor = (elem, { r, g, b }) =>
elem.style.backgroundColor = `rgb(${r}, ${g}, ${b})`
const c = new Color () // { r: 128, g: 128, b: 128 }
setpColor (c) // { r: 128, g: 128, b: 136 }
Now we have a way to create colors, get the "next" color, and we can set the color of an HTML element
Lastly, we write helpers delay
and effect
. delay
will create a Promised value that resolves in ms
milliseconds. effect
is used for functions which have a side effect (like setting the property of an HTML element). and FPS
is just our frames-per-second constant
const delay = (x, ms) =>
new Promise (r => setTimeout (r, ms, x))
const effect = f => x =>
(f (x), x)
const FPS =
1000 / 30
To run the program, just call main
with an input element. Because it's an asynchronous program, don't forget to handle both the success and errors cases. When the program finally stops, the last used color will be output.
main (document.querySelector('#main'))
.then (console.log, console.error)
// => { Color r: 136, g: 8, b: 40 }
To stop the program, just set RUNNING = false
at any time
// stop after 5 seconds
setTimeout (() => RUNNING = false, 5000)
Here's a working demo
const Color = (r = 128, g = 128, b = 128) =>
({ r, g, b })
const stepColor = ({ r, g, b }, step = 16) =>
b < 255
? Color (r, g, b + step)
: g < 255
? Color (r, g + step, 0)
: r < 255
? Color (r + step, 0, 0)
: Color (0, 0, 0)
const setElemColor = (elem, { r, g, b }) =>
elem.style.backgroundColor = `rgba(${r}, ${g}, ${b}, 1)`
const delay = (x, ms) =>
new Promise (r => setTimeout (r, ms, x))
const effect = f => x =>
(f (x), x)
const FPS =
1000 / 60
let RUNNING =
true
const main = async (elem, color = Color ()) =>
RUNNING
? delay (color, FPS)
.then (effect (color => setElemColor (elem, color)))
.then (color => main (elem, stepColor (color)))
: color
main (document.querySelector('#main'))
.then (console.log, console.error)
// => { r: 136, g: 8, b: 40 }
// stop after 5 seconds
setTimeout (() => RUNNING = false, 5000)
#main {
width: 100px;
height: 100px;
background-color: rgb(128, 128, 128);
}
<div id="main"></div>
<p>runs for 5 seconds...</p>