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>