Class: Philiprehberger::Debounce::Debouncer

Inherits:
Object
  • Object
show all
Defined in:
lib/philiprehberger/debounce/debouncer.rb

Overview

Delays execution until the wait period elapses without new calls.

When #call is invoked, any pending execution is cancelled and the timer restarts. The block only fires once the caller stops calling for the full wait duration.

Examples:

debouncer = Philiprehberger::Debounce.debounce(wait: 0.3) { puts "saved" }
debouncer.call   # resets timer
debouncer.call   # resets timer again — block fires 0.3s after this call

Instance Method Summary collapse

Constructor Details

#initialize(wait:, leading: false, trailing: true, max_wait: nil, on_execute: nil, on_cancel: nil, on_flush: nil, on_error: nil, &block) ⇒ Debouncer

Returns a new instance of Debouncer.

Parameters:

  • wait (Float)

    delay in seconds

  • leading (Boolean) (defaults to: false)

    fire on the leading edge

  • trailing (Boolean) (defaults to: true)

    fire on the trailing edge

  • max_wait (Float, nil) (defaults to: nil)

    maximum time to wait before forcing execution

  • on_execute (Proc, nil) (defaults to: nil)

    callback after block executes, receives return value

  • on_cancel (Proc, nil) (defaults to: nil)

    callback when cancel is invoked

  • on_flush (Proc, nil) (defaults to: nil)

    callback when flush is invoked

  • block (Proc)

    the block to execute

Raises:

  • (ArgumentError)


24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/philiprehberger/debounce/debouncer.rb', line 24

def initialize(wait:, leading: false, trailing: true, max_wait: nil, on_execute: nil, on_cancel: nil, on_flush: nil, on_error: nil, &block)
  raise ArgumentError, 'block is required' unless block
  raise ArgumentError, 'wait must be positive' unless wait.positive?
  raise ArgumentError, 'at least one of leading or trailing must be true' if !leading && !trailing
  raise ArgumentError, 'max_wait must be positive' if max_wait && !max_wait.positive?

  @wait = wait
  @leading = leading
  @trailing = trailing
  @max_wait = max_wait
  @on_execute = on_execute
  @on_cancel = on_cancel
  @on_flush = on_flush
  @on_error = on_error
  @block = block
  @mutex = Mutex.new
  @pending = false
  @last_args = nil
  @called_leading = false
  @generation = 0
  @call_count = 0
  @execution_count = 0
  @first_call_time = nil
  @last_result = nil
end

Instance Method Details

#call(*args) ⇒ void

This method returns an undefined value.

Invoke the debouncer with optional arguments.

Resets the internal timer. The block will execute after wait seconds of inactivity (trailing edge) or immediately on the first call (leading edge).

Parameters:

  • args (Array)

    arguments forwarded to the block



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/philiprehberger/debounce/debouncer.rb', line 57

def call(*args)
  @mutex.synchronize do
    @call_count += 1
    @last_args = args
    @pending = true
    @generation += 1
    current_gen = @generation

    @first_call_time ||= monotonic_now

    # Leading edge: fire immediately on the first call of a new cycle
    if @leading && !@called_leading
      @called_leading = true
      execute(args)
    end

    # Check if max_wait has been exceeded
    if @max_wait && @first_call_time && (monotonic_now - @first_call_time) >= @max_wait
      if @trailing
        args_to_use = @last_args
        @pending = false
        @last_args = nil
        @called_leading = false
        @first_call_time = nil
        execute(args_to_use)
      end
      return
    end

    # Start a new trailing timer
    if @trailing || @leading
      effective_wait = @wait
      if @max_wait && @first_call_time
        remaining_max = @max_wait - (monotonic_now - @first_call_time)
        effective_wait = [effective_wait, remaining_max].min if remaining_max.positive?
      end

      Thread.new do
        sleep effective_wait

        @mutex.synchronize do
          # Only fire if no new calls happened since this timer started
          if @generation == current_gen && @pending
            if @trailing
              args_to_use = @last_args
              @pending = false
              @last_args = nil
              @called_leading = false
              @first_call_time = nil
              execute(args_to_use)
            else
              @pending = false
              @called_leading = false
              @first_call_time = nil
            end
          end
        end
      end
    end
  end
end

#cancelvoid

This method returns an undefined value.

Cancel any pending execution.



122
123
124
125
126
127
128
129
130
131
132
# File 'lib/philiprehberger/debounce/debouncer.rb', line 122

def cancel
  @mutex.synchronize do
    @generation += 1
    @pending = false
    @last_args = nil
    @called_leading = false
    @first_call_time = nil
  end

  invoke_callback(@on_cancel)
end

#flushvoid

This method returns an undefined value.

Execute the pending block immediately and cancel the timer.



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/philiprehberger/debounce/debouncer.rb', line 137

def flush
  args = nil
  should_execute = false

  @mutex.synchronize do
    if @pending
      args = @last_args
      should_execute = true
      @generation += 1
      @pending = false
      @last_args = nil
      @called_leading = false
      @first_call_time = nil
    end
  end

  execute(args) if should_execute
  invoke_callback(@on_flush)
end

#last_resultObject?

Returns the result of the last block execution.

Returns:

  • (Object, nil)


189
190
191
# File 'lib/philiprehberger/debounce/debouncer.rb', line 189

def last_result
  @mutex.synchronize { @last_result }
end

#metricsHash

Returns metrics about debouncer usage.

Returns:

  • (Hash)

    call_count, execution_count, suppressed_count



176
177
178
179
180
181
182
183
184
# File 'lib/philiprehberger/debounce/debouncer.rb', line 176

def metrics
  @mutex.synchronize do
    {
      call_count: @call_count,
      execution_count: @execution_count,
      suppressed_count: @call_count - @execution_count
    }
  end
end

#pending?Boolean

Whether there is a pending execution.

Returns:

  • (Boolean)


160
161
162
# File 'lib/philiprehberger/debounce/debouncer.rb', line 160

def pending?
  @mutex.synchronize { @pending }
end

#pending_argsArray?

Returns the arguments that would be passed to the next execution.

Returns:

  • (Array, nil)

    the pending arguments, or nil if not pending



167
168
169
170
171
# File 'lib/philiprehberger/debounce/debouncer.rb', line 167

def pending_args
  @mutex.synchronize do
    @pending ? @last_args : nil
  end
end

#reset_metricsvoid

This method returns an undefined value.

Resets all metric counters to zero.



196
197
198
199
200
201
# File 'lib/philiprehberger/debounce/debouncer.rb', line 196

def reset_metrics
  @mutex.synchronize do
    @call_count = 0
    @execution_count = 0
  end
end