class Debouncer

Constants

DEFAULT_GROUP
EMPTY
VERSION

Attributes

delay[R]

Public Class Methods

new(delay, &block) click to toggle source
# File lib/debouncer.rb, line 14
def initialize(delay, &block)
  self.delay = delay
  raise ArgumentError, 'Expected a block' unless block
  @timeouts = {}
  @threads  = []
  @rescuers = {}
  @block    = block
  @lock     = Mutex.new
end

Public Instance Methods

[](*args, &block)
Alias for: call
arity() click to toggle source
# File lib/debouncer.rb, line 29
def arity
  @block.arity
end
call(*args, &block) click to toggle source
# File lib/debouncer.rb, line 47
def call(*args, &block)
  call_with_id DEFAULT_GROUP, *args, &block
end
Also aliased as: []
call_with_id(id, *args, &block) click to toggle source
# File lib/debouncer.rb, line 52
def call_with_id(id, *args, &block)
  args << block if block
  run_thread = nil
  exclusively do
    thread        = @timeouts[id] ||= new_thread { begin_delay id, args }
    @flush        = [id]
    old_args      = thread[:args]
    thread[:args] =
        if @reducer
          initial, reducer = @reducer
          old_args         ||= initial || []
          if reducer.is_a? Symbol
            old_args.__send__ reducer, args
          elsif reducer.respond_to? :call
            reducer.call old_args, args, id
          end
        else
          args.empty? ? old_args : args
        end
    if @flush.is_a? Array
      thread[:run_at] = Time.now + @delay
    else
      thread.kill
      @timeouts.delete id
      @threads.delete thread
      run_thread = new_thread { run_block thread }
      run_thread = nil unless @flush
      @flush = false
    end
  end
  run_thread.join if run_thread
  self
end
delay=(delay) click to toggle source
# File lib/debouncer.rb, line 24
def delay=(delay)
  raise ArgumentError, "Expected Numeric, but got #{delay.class.name}" unless delay.is_a? Numeric
  @delay = delay
end
flush(id = EMPTY, and_join: false) click to toggle source
# File lib/debouncer.rb, line 86
def flush(id = EMPTY, and_join: false)
  if @lock.owned?
    raise ArgumentError, 'You cannot flush other groups from inside a reducer' unless id == EMPTY || [id] == @flush
    @flush = and_join
  elsif id == EMPTY
    flush @timeouts.keys.first while @timeouts.any?
  else
    thread = exclusively do
      if (old_thread = @timeouts.delete(id))
        old_thread.kill
        @threads.delete old_thread
        new_thread { run_block old_thread }
      end
    end
    thread.join if thread
  end
  self
end
flush!(*args) click to toggle source
# File lib/debouncer.rb, line 105
def flush!(*args)
  flush *args, and_join: true
end
group(id) click to toggle source
# File lib/debouncer.rb, line 43
def group(id)
  Group.new self, id
end
inspect_params() click to toggle source
# File lib/debouncer.rb, line 128
def inspect_params
  {delay: @delay, timeouts: @timeouts.count, threads: @threads.count}
end
join(id = EMPTY, kill_first: false) click to toggle source
# File lib/debouncer.rb, line 109
def join(id = EMPTY, kill_first: false)
  if id == EMPTY
    while (thread = exclusively { @threads.find &:alive? })
      thread.kill if kill_first
      thread.join
    end
    exclusively { [@threads, @timeouts].each &:clear } if kill_first
  elsif (thread = exclusively { @timeouts.delete id })
    @threads.delete thread
    thread.kill if kill_first
    thread.join
  end
  self
end
kill(id = EMPTY) click to toggle source
# File lib/debouncer.rb, line 124
def kill(id = EMPTY)
  join id, kill_first: true
end
reducer(*initial, &block) click to toggle source
# File lib/debouncer.rb, line 33
def reducer(*initial, &block)
  @reducer = [initial, block || initial.pop]
  self
end
rescuer(kind = StandardError, &block) click to toggle source
# File lib/debouncer.rb, line 38
def rescuer(kind = StandardError, &block)
  @rescuers[kind] = block
  self
end
runs_at(id = DEFAULT_GROUP) click to toggle source
# File lib/debouncer.rb, line 140
def runs_at(id = DEFAULT_GROUP)
  thread = @timeouts[id]
  thread && thread[:run_at]
end
sleeping?() click to toggle source
# File lib/debouncer.rb, line 136
def sleeping?
  @timeouts.length.nonzero?
end
to_proc() click to toggle source
# File lib/debouncer.rb, line 132
def to_proc
  method(:call).to_proc
end

Private Instance Methods

begin_delay(id, args) click to toggle source
# File lib/debouncer.rb, line 147
def begin_delay(id, args)
  thread[:run_at] = Time.now + @delay
  thread[:args] ||= args
  sleep @delay
  until exclusively { (thread[:run_at] <= Time.now).tap { |ready| @timeouts.delete id if ready } }
    sleep [thread[:run_at] - Time.now, 0].max
  end
  run_block thread
rescue => ex
  @timeouts.reject! { |_, v| v == thread }
  (rescuer = @rescuers.find { |klass, _| ex.is_a? klass }) && rescuer.last[ex]
ensure
  exclusively { @threads.delete thread }
end
exclusively(&block) click to toggle source
# File lib/debouncer.rb, line 170
def exclusively(&block)
  @lock.synchronize &block
end
new_thread(*args, &block) click to toggle source
# File lib/debouncer.rb, line 166
def new_thread(*args, &block)
  Thread.new(*args, &block).tap { |t| @threads << t }
end
run_block(thread) click to toggle source
# File lib/debouncer.rb, line 162
def run_block(thread)
  @block.call *thread[:args]
end
thread() click to toggle source
# File lib/debouncer.rb, line 174
def thread
  Thread.current
end