Search code examples
javascriptember.jsember-clidebouncing

Using Ember observers with a timed queue drain


I'm attempting to build a "notification system" of sorts for a broadcasting overlay. Essentially I catch events, add them to an array, then I run a function that observes that array. Each time an event is added, I run an animation that takes a given amount of time, remove that from the array and then move onto the next one.

I found that debounce gets me a part of the way there. I'm able to add any number of events while the animations are running and it's able to empty the queue.

Problem being, I have to wait that specified time (in this case 5 seconds) before the first event is handled. However, when I set debounce to be immediate, everything breaks in that the first event will be immediately handled, but nothing else will.

# Pool handling.
pool: []

# Function adds events to the pool array.
addEventToPool: (event, data) ->
  console.log "added #{event} with #{data} to pool!"
  @get('pool').pushObject(data)

# Function that observes the pool array and runs debounce 
# if there are any items in the pool.
observePool: (->
  Ember.run.debounce(@, @handleEvent, 5000, false) if @get('pool').length
).observes('pool.[]')

# Event handling.
handleEvent: ->
  pool = @get('pool')
  object = pool.get('firstObject')
  @set('payload', object)

  Ember.$(".event-message__#{object.event}").addClass('active')

  Ember.run.later (->
    Ember.$(".event-message__#{object.event}").removeClass('active')
    pool.removeObject(object)
  ), 2000

  console.log "Number of remaining objects: #{pool.length}."
  console.log "Objects remaining: #{JSON.stringify pool}."

I have a feeling that I need to move off of debounce to fix this, but I'm not sure what that solution is.

Please let me know if you need any clarification!


Solution

  • Ember.run.debounce

    The purpose of Ember.run.debounce is to only run something once if debounce has not been called in the last X seconds.

    It's main uses are for things like kicking off some type of action after a user has finished typing - so on every character someone is typing you could call Ember.run.debounce(handleInput, 1000) and you can be sure that no matter how many times they press a key, your function will only run once - after they haven't pressed a key for 1 second.

    It's also useful for handling scroll events - where there are lets say hundreds of scroll events when scrolling on a page but you only want to do some action once the scrolling has stopped - calling debounce hundreds of times doesn't run it until you've stopped calling debounce for the timeout value (e.g. 100ms after the last scroll event in this case).

    This seems a little bit different to what you're trying to do.

    Suggested Solution

    I think what you want to do is use Ember.run.later. You could combine it all into one function that simply observes pool.firstObject - since you're always pulling the first object and removing it once you're done.

    handleEvent: Ember.observer('pool.firstObject', function() {
      var pool = this.get('pool');
      var obj = pool.get('firstObject');
      if (obj) {
        // add class
        Ember.$(".event-message__#{object.event}").addClass('active');
    
        // schedule remove class for 2 seconds from now
        Ember.run.later(function() {
          Ember.$(".event-message__#{object.event}").removeClass('active');
        }, 2000);
    
        // schedule remove object from pool 5 seconds from now
        Ember.run.later(function() {
          pool.removeObject(obj);
          // after you've removed this object (which was firstObject)
          // pool.firstObject will change, and the handleEvent function 
          // will get kicked off again
        }, 5000);
    
      }
    })