The project is in a healthy, maintained state
Debounce delays execution until a quiet period elapses. Throttle limits execution frequency. Both are thread-safe with leading/trailing edge options and cancel/flush control.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

philiprehberger-debounce

Tests Gem Version Last updated

Debounce and throttle decorators for Ruby method calls

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem 'philiprehberger-debounce'

Or install directly:

gem install philiprehberger-debounce

Usage

require 'philiprehberger/debounce'

# Debounce: delays execution until 0.5s of inactivity
debouncer = Philiprehberger::Debounce.debounce(wait: 0.5) { |query| search(query) }
debouncer.call('ruby')
debouncer.call('ruby gems')   # resets the timer
debouncer.call('ruby gems 3') # only this one fires after 0.5s

Throttle

# Throttle: executes at most once per second
throttler = Philiprehberger::Debounce.throttle(interval: 1.0) { |event| log(event) }
throttler.call('click') # fires immediately
throttler.call('click') # ignored (within interval)
sleep 1.0
throttler.call('click') # fires again

Leading and Trailing Edges

# Leading edge: fire immediately, ignore subsequent calls during wait
debouncer = Philiprehberger::Debounce.debounce(wait: 0.3, leading: true, trailing: false) do |v|
  puts v
end

# Both edges: fire immediately AND after the quiet period
debouncer = Philiprehberger::Debounce.debounce(wait: 0.3, leading: true, trailing: true) do |v|
  puts v
end

Cancel and Flush

debouncer = Philiprehberger::Debounce.debounce(wait: 1.0) { save_draft }

debouncer.call
debouncer.cancel # cancel pending execution

debouncer.call
debouncer.flush  # execute immediately without waiting

Max Wait

# Force execution after 3 seconds even if calls keep arriving
debouncer = Philiprehberger::Debounce.debounce(wait: 0.5, max_wait: 3.0) do |query|
  search(query)
end

Execution Callbacks

debouncer = Philiprehberger::Debounce.debounce(
  wait: 0.5,
  on_execute: ->(result) { logger.info("Executed: #{result}") },
  on_cancel: -> { logger.info('Cancelled') },
  on_flush: -> { logger.info('Flushed') }
) { |query| search(query) }

Metrics

debouncer = Philiprehberger::Debounce.debounce(wait: 0.5) { |q| search(q) }

10.times { debouncer.call('test') }
sleep 0.6

debouncer.metrics
# => { call_count: 10, execution_count: 1, suppressed_count: 9 }

debouncer.reset_metrics

Pending Args

debouncer = Philiprehberger::Debounce.debounce(wait: 1.0) { |q| search(q) }

debouncer.call('ruby')
debouncer.pending_args # => ['ruby']
debouncer.cancel
debouncer.pending_args # => nil

Keyed Debouncing

# Debounce per key independently
keyed = Philiprehberger::Debounce.keyed(wait: 0.5) { |query| search(query) }

keyed.call(:user_1, 'ruby')   # debounces independently
keyed.call(:user_2, 'python') # debounces independently

keyed.pending_keys  # => [:user_1, :user_2]
keyed.cancel(:user_1)
keyed.cancel_all

Rate Limiting

# Allow at most 5 requests per 10-second window
limiter = Philiprehberger::Debounce.rate_limiter(limit: 5, window: 10)

result = limiter.call(:user_1)
# => { allowed: true, remaining: 4, retry_after: 0 }

# After exceeding the limit:
# => { allowed: false, remaining: 0, retry_after: 7.3 }

limiter.reset(:user_1) # clear history for a key

Coalescing

# Collect arguments from multiple calls and flush as a batch
coalescer = Philiprehberger::Debounce.coalesce(wait: 0.5) do |batched_args|
  bulk_insert(batched_args)
end

coalescer.call('row1')
coalescer.call('row2')
coalescer.call('row3')
# After 0.5s of inactivity, block fires with [['row1'], ['row2'], ['row3']]

coalescer.flush          # fire immediately with queued args
coalescer.cancel         # discard queued args
coalescer.pending_count  # number of queued calls

Last Result

debouncer = Philiprehberger::Debounce.debounce(wait: 0.1) { |x| x.upcase }
debouncer.call('hello')
sleep 0.15
debouncer.last_result # => "HELLO"

throttler = Philiprehberger::Debounce.throttle(interval: 0.1) { |x| x * 2 }
throttler.call(5)
throttler.last_result # => 10

Mixin

class SearchController
  include Philiprehberger::Debounce::Mixin

  def search(query)
    # expensive search operation
  end
  debounce_method :search, wait: 0.5

  def log_event(event)
    # logging
  end
  throttle_method :log_event, interval: 1.0
end

API

Philiprehberger::Debounce

Method Description
.debounce(wait:, leading: false, trailing: true, max_wait: nil, on_execute: nil, on_cancel: nil, on_flush: nil, &block) Create a debouncer that delays execution
.throttle(interval:, leading: true, trailing: false, on_execute: nil, on_cancel: nil, on_flush: nil, &block) Create a throttler that limits execution rate
.keyed(wait:, leading: false, trailing: true, max_wait: nil, on_execute: nil, on_cancel: nil, on_flush: nil, &block) Create a keyed debouncer for per-key debouncing
.rate_limiter(limit:, window:) Create a sliding window rate limiter
.coalesce(wait:, &block) Create a coalescer that batches arguments

Debouncer

Method Description
#call(*args) Invoke the debouncer, resetting the timer
#cancel Cancel any pending execution
#flush Execute immediately if pending
#pending? Whether an execution is pending
#pending_args Returns the pending arguments, or nil
#metrics Returns { call_count:, execution_count:, suppressed_count: }
#reset_metrics Resets all metric counters to zero
#last_result Returns the result of the last block execution

Throttler

Method Description
#call(*args) Invoke the throttler, rate-limited
#cancel Cancel any pending trailing execution
#flush Execute immediately if pending
#pending? Whether a trailing execution is pending
#pending_args Returns the pending arguments, or nil
#metrics Returns { call_count:, execution_count:, suppressed_count: }
#reset_metrics Resets all metric counters to zero
#last_result Returns the result of the last block execution

KeyedDebouncer

Method Description
#call(key, *args) Invoke the debouncer for a specific key
#cancel(key) Cancel pending execution for a specific key
#cancel_all Cancel all pending executions
#pending_keys List keys with pending executions

RateLimiter

Method Description
#call(key = :default) Check rate limit, returns { allowed:, remaining:, retry_after: }
#reset(key = :default) Clear request history for a key

Coalescer

Method Description
#call(*args) Queue arguments for the next batch
#flush Fire the block immediately with queued args
#cancel Discard all queued arguments
#pending_count Number of queued calls

Mixin

Method Description
.debounce_method(name, wait:, leading: false, trailing: true) Debounce an instance method
.throttle_method(name, interval:, leading: true, trailing: false) Throttle an instance method

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT