'ExpressJS backend put requests into a queue

I have clients sending tasks to be performed by the server but these requests should be handled in a queue like fashion. Any idea how I can do this? Thanks.

    express.Router().post('/tasks', function(req, res){
      //This is the task to perform. While being performed, another user
      //might send a request AND should only be processed when this is done.
      //This should also flag the pending task if it is completed.

      Promise.resolve(req.body)
      .then(function() {
      //..
      })
      .catch(function(error) {
        //....
      })

    })


Solution 1:[1]

@BenjaminGruenbaum answer is superb but not altogether clear. Here's additional clarity and a working snippet (proof of the pudding):

const queue = (fn) => {
  let q = Promise.resolve()
  return (...args) => {
    q = q.then(() => fn(...args)).catch(() => {
      // noop
    })
    return q
  }
}

const speak = (sayWhat, delay) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('say', sayWhat)
      resolve()
    }, delay)
  })
}

const speakQueued = queue(speak)

const helloBtn = document.getElementById('hello-btn')
const byeBtn = document.getElementById('bye-btn')

helloBtn.onclick = () => speakQueued('hello there', 1000)
byeBtn.onclick = () => speakQueued('bye bye', 2000)
<button id="hello-btn">say hello (1 sec wait)</button>
<button id="bye-btn">say bye (2 sec wait)</button>

Solution 2:[2]

Here's more of an object oriented approach to the problem. This gives more control, for example, only queueing items which have not previously been seen.

class QueueUnique {
  items = []
  q
  func

  constructor(func) {
    this.q = Promise.resolve()
    this.func = func
  }

  add(item) {
    const done = this.items.find(itm => itm.id === item.id)
    if (done) {
      console.log(`not adding item ${item.id} because it has already been queued`)
    } else {
      const queuedFunc = this.queue(item)
      queuedFunc()
      this.items.push(item)
    }
  }

  queue(item) {
    return () => {
      this.q = this.q.then(() => this.func(item)).catch(() => {
        // noop
      })
      return this.q
    }
  }
}

const speak = (item) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('say', item.sayWhat)
      resolve()
    }, item.delay)
  })
}

const queue = new QueueUnique(speak)

const btn1Click = () => {
  queue.add({id: 1, sayWhat: 'one', delay: 1000})
}

const btn2Click = () => {
  queue.add({id: 2, sayWhat: 'two', delay: 2000})
}

const btn3Click = () => {
  queue.add({id: 3, sayWhat: 'three', delay: 3000})
}

const btn4Click = () => {
  queue.add({id: 4, sayWhat: 'four', delay: 4000})
}

const btn5Click = () => {
  queue.add({id: 5, sayWhat: 'five', delay: 5000})
}
<button onclick="btn1Click()">button 1</button>
<button onclick="btn2Click()">button 2</button>
<button onclick="btn3Click()">button 3</button>
<button onclick="btn4Click()">button 4</button>
<button onclick="btn5Click()">button 5</button>

Solution 3:[3]

Here's another object oriented solution with another working example which allows you to view the queued items. This solution does not use promise chaining (but still requires func to return a promise).

const elItems = document.getElementById('items')
const elSuccess = document.getElementById('success')
const elFailure = document.getElementById('failure')

class QueueUnique {
  items = []
  success = []
  failure = []
  processing = false
  func

  constructor(func) {
    this.func = func
  }

  add(item) {
    const allItems = [...this.items, ...this.success, ...this.failure]
    const addItem = !allItems.some(itm => itm.id === item.id)
    if (!addItem) {
      console.log(`QueueUnique not adding item ${item.id} because it has already been queued`)
    } else {
      this.items = [...this.items, item]
      console.log(`QueueUnique adding item ${item.id} to queue position ${this.items.length}`)
      this.process()
    }
  }

  process() {
    this.update() // this method is not needed and just updates the arrays shown on screen

    if (!this.processing && this.items.length) {
      this.processing = true
      const item = this.items[0]
      console.log(`QueueUnique start executing item ${item.id}`)
      this.func(item).then(() => {
        console.log(`QueueUnique success executing item ${item.id}`)
        this.complete(true)
      }).catch(err => {
        console.log(`QueueUnique failure executing item ${item.id} - ${err.message}`)
        this.complete(false)
      })
    }
  }

  complete(success) {
    const item = this.items[0]
    this.items = this.items.filter(itm => itm.id !== item.id)
    if (success) {
      this.success = [...this.success, item]
    } else {
      this.failure = [...this.failure, item]
    }
    this.processing = false
    this.process()
  }
  
  update() {
    elItems.innerHTML = this.items.map(item => item.id)
    elSuccess.innerHTML = this.success.map(item => item.id)
    elFailure.innerHTML = this.failure.map(item => item.id)
  }
}

const speak = (item) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('say', item.sayWhat)
      resolve()
    }, item.delay)
  })
}

const queue = new QueueUnique(speak)

const btn1Click = () => {
  queue.add({id: 1, sayWhat: 'one', delay: 1000})
}

const btn2Click = () => {
  queue.add({id: 2, sayWhat: 'two', delay: 2000})
}

const btn3Click = () => {
  queue.add({id: 3, sayWhat: 'three', delay: 3000})
}

const btn4Click = () => {
  queue.add({id: 4, sayWhat: 'four', delay: 4000})
}

const btn5Click = () => {
  queue.add({id: 5, sayWhat: 'five', delay: 5000})
}
div {
  display: inline-block;
  margin: 5px 0 0 10px;
  padding: 2px 5px;
  border-radius: 3px;
  background-color: #DDD;
}
<button onclick="btn1Click()">button 1</button>
<button onclick="btn2Click()">button 2</button>
<button onclick="btn3Click()">button 3</button>
<button onclick="btn4Click()">button 4</button>
<button onclick="btn5Click()">button 5</button>
<div>items <span id="items"></span></div>
<div>success <span id="success"></span></div>
<div>failure <span id="failure"></span></div>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 danday74
Solution 2 danday74
Solution 3