debouncer 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +171 -2
- data/lib/debouncer.rb +95 -52
- data/lib/debouncer/debounceable.rb +25 -49
- data/lib/debouncer/group.rb +36 -0
- data/lib/debouncer/inspection.rb +13 -0
- data/lib/debouncer/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 48c4780e6aab992ab7ecf77f240897bd9e7474f4
|
4
|
+
data.tar.gz: 8e311281e918f6bf12e0f8051e30be08638d1424
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 969290d4ad850a13892552d0ee8e66ad442a2bbfbf736220f1596a4d8e3cd4d28b28b3d01566d01fe31496b8c11ead1a8cb943a5444512f698e7cd9a1f451ed5
|
7
|
+
data.tar.gz: 84226ab4da39584b1c96a7e3c81b535514c7b5eaf7ca1c83bc23b016544c038169d16975732f1f72e6dfd3f85f6d670feb975c38c991b5e3758155484ae2373a
|
data/README.md
CHANGED
@@ -20,7 +20,176 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
## Usage
|
22
22
|
|
23
|
-
|
23
|
+
### The easy way
|
24
|
+
|
25
|
+
Generally all you want to do is debounce an instance or class method. The `Debounceable` module gives you everything you need to get your bouncing methods debounced.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
require 'debouncer/debounceable'
|
29
|
+
|
30
|
+
class DangerZone
|
31
|
+
extend Debouncer::Debounceable
|
32
|
+
|
33
|
+
def send_warning
|
34
|
+
system "echo We are about to get crazy! | wall"
|
35
|
+
end
|
36
|
+
|
37
|
+
debounce :send_warning, 2
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
Each call to `send_warning` will be delayed on a background thread for 2 seconds before firing. If, during those two seconds, it's called again, the count-down will restart, and only one warning will actually be sent.
|
42
|
+
|
43
|
+
If you want to send your warning immediately, your original method has been given a suffix so you can still access it:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
DangerZone.new.send_warning_immediately
|
47
|
+
```
|
48
|
+
|
49
|
+
A couple of methods have also been added to help work with background threads:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
dz = DangerZone.new
|
53
|
+
|
54
|
+
dz.join_send_warning # Wait for all warnings to be sent
|
55
|
+
dz.flush_send_warning # Send warnings immediately if any are waiting
|
56
|
+
dz.cancel_send_warning # Cancel all waiting warnings
|
57
|
+
```
|
58
|
+
|
59
|
+
You can also debounce your calls in groups. By setting the `grouped: true` option, the first argument passed to the debounced method will be used to create a separate debouncer thread.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
class DangerZone
|
63
|
+
extend Debouncer::Debounceable
|
64
|
+
|
65
|
+
def send_warning(message)
|
66
|
+
system "echo #{message.shellescape} | wall"
|
67
|
+
end
|
68
|
+
|
69
|
+
debounce :send_warning, 2, grouped: true
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
Now, each unique message will have its own separate timeout. You can also pass group identifiers to `join_`, `flush_`, and `cancel_` methods to affect only those groups.
|
74
|
+
|
75
|
+
Arguments are always passed to your original method intact, and by default, the last set of arguments in a group wins. If the example above didn't use grouping:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
d = DangerZone.new
|
79
|
+
d.send_warning "We're going down!"
|
80
|
+
d.send_warning "Spiders are attacking!"
|
81
|
+
```
|
82
|
+
|
83
|
+
The first warning would be replaced by the second when the method is eventually run.
|
84
|
+
|
85
|
+
You can combine arguments to produce an end result however you like, using a reducer:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class DangerZone
|
89
|
+
extend Debouncer::Debounceable
|
90
|
+
|
91
|
+
def send_warning(*messages)
|
92
|
+
system "echo #{messages.map(&:shellescape).join ';'} | wall"
|
93
|
+
end
|
94
|
+
|
95
|
+
debounce :send_warning, 2, reduce_with: :combine_messages
|
96
|
+
|
97
|
+
def combine_messages(memo, messages)
|
98
|
+
memo + messages
|
99
|
+
end
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
The `combine_messages` method will be called whenever `send_warning` is called. The first argument is the last value it returned, or an empty array if this is the first call for the thread. The second argument is an array of the arguments supplied to `send_warning`. It should return an array of arguments that will ultimately be passed to the original method.
|
104
|
+
|
105
|
+
A reducer method is a good place to call `flush_*` if you hit some sort of limit or threshold. Just remember to still return the array of arguments you want to call.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
def combine_messages(memo, messages)
|
109
|
+
result = memo + messages
|
110
|
+
flush_send_warning if result.length >= 5
|
111
|
+
result
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
Finally, you can also debounce class/module methods using the `mdebounce` method. If you want to combine calls on various instances of a class, a sound pattern is to debounce a class method and have instances call it. For example, consider broadcasting data changes to browsers, where you want to group changes to the same model together into single broadcasts:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
class Record
|
119
|
+
extend Debouncer::Debounceable
|
120
|
+
|
121
|
+
def self.broadcast(record_id)
|
122
|
+
look_up(record_id).broadcast
|
123
|
+
end
|
124
|
+
|
125
|
+
mdebounce :broadcast, 0.5, grouped: true
|
126
|
+
|
127
|
+
def save
|
128
|
+
write_to_database
|
129
|
+
Record.broadcast id
|
130
|
+
end
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
In a web application, it's common for several instances of a model representing the same data record to existing during the course of a request. Debouncing the `broadcast` method on the instance wouldn't be effective, since each instance would have its own debouncer. By using a debounced class method, records are grouped by their ID instead of the Ruby objects that represent them.
|
135
|
+
|
136
|
+
### The clean(er) way
|
137
|
+
|
138
|
+
Under the hood, the `Debounceable` module uses a `Debouncer` instance as a thread controller. If you prefer not to have any extra methods defined on your class, you can use a `Debouncer` instance to achieve the same results.
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
require 'debouncer'
|
142
|
+
|
143
|
+
d = Debouncer.new(2) { |message| puts message }
|
144
|
+
d.call 'I have arrived'
|
145
|
+
```
|
146
|
+
|
147
|
+
This will print the message "I have arrived!" after 2 seconds.
|
148
|
+
|
149
|
+
Grouping is simple with a debouncer:
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
d.group(:warnings).call 'I am about to arrive...'
|
153
|
+
d.group(:alerts).call 'I have arrived!'
|
154
|
+
d.group(:alerts).flush
|
155
|
+
```
|
156
|
+
|
157
|
+
When adding an argument reducer, you can also specify an initial value:
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
d = Debouncer.new(2) { |*messages| puts messages }
|
161
|
+
d.reducer('Here are some messages:', '') { |memo, messages| memo + messages }
|
162
|
+
d.call "Evented Ruby isn't so bad"
|
163
|
+
d.call "And it's threaded, too!"
|
164
|
+
```
|
165
|
+
|
166
|
+
After 2 seconds, the above code will print:
|
167
|
+
|
168
|
+
```text
|
169
|
+
Here are some messages:
|
170
|
+
|
171
|
+
Evented Ruby isn't so bad
|
172
|
+
And it's threaded, too!
|
173
|
+
```
|
174
|
+
|
175
|
+
If your reducer simply adds or OR's two arrays together, you can use a symbol instead:
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
d.reducer 'Here are some messages:', '', :+
|
179
|
+
```
|
180
|
+
|
181
|
+
This will have the same result as the block form. If you use `:|` instead of `:+`, it will be like `memo | messages`, so messages won't be repeated between calls.
|
182
|
+
|
183
|
+
Methods like `flush`, `kill`, and `join` are available too, and to exactly what you'd expect. You can call them directly on your debouncer, or after a `group(id)` call if you want to target a specific group.
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
# These lines are equivalent:
|
187
|
+
d.join :messages
|
188
|
+
d.group(:messages).join
|
189
|
+
|
190
|
+
# This will join all threads:
|
191
|
+
d.join
|
192
|
+
```
|
24
193
|
|
25
194
|
## Development
|
26
195
|
|
@@ -30,7 +199,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
30
199
|
|
31
200
|
## Contributing
|
32
201
|
|
33
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
202
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/hx/debouncer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
34
203
|
|
35
204
|
|
36
205
|
## License
|
data/lib/debouncer.rb
CHANGED
@@ -1,23 +1,37 @@
|
|
1
1
|
require 'debouncer/version'
|
2
|
+
require 'debouncer/inspection'
|
3
|
+
require 'debouncer/group'
|
4
|
+
require 'debouncer/debounceable'
|
2
5
|
|
3
6
|
class Debouncer
|
7
|
+
include Inspection
|
8
|
+
|
9
|
+
DEFAULT_GROUP = Object.new
|
10
|
+
EMPTY = Object.new
|
11
|
+
|
12
|
+
attr_reader :delay
|
13
|
+
|
4
14
|
def initialize(delay, &block)
|
5
|
-
|
6
|
-
|
15
|
+
self.delay = delay
|
16
|
+
raise ArgumentError, 'Expected a block' unless block
|
7
17
|
@timeouts = {}
|
8
18
|
@threads = []
|
9
|
-
@lock = Mutex.new
|
10
19
|
@rescuers = {}
|
11
|
-
block
|
20
|
+
@block = block
|
21
|
+
@lock = Mutex.new
|
12
22
|
end
|
13
23
|
|
14
|
-
def
|
15
|
-
|
16
|
-
|
24
|
+
def delay=(delay)
|
25
|
+
raise ArgumentError, "Expected Numeric, but got #{delay.class.name}" unless delay.is_a? Numeric
|
26
|
+
@delay = delay
|
17
27
|
end
|
18
28
|
|
19
|
-
def
|
20
|
-
@
|
29
|
+
def arity
|
30
|
+
@block.arity
|
31
|
+
end
|
32
|
+
|
33
|
+
def reducer(*initial, &block)
|
34
|
+
@reducer = [initial, block || initial.pop]
|
21
35
|
self
|
22
36
|
end
|
23
37
|
|
@@ -26,76 +40,111 @@ class Debouncer
|
|
26
40
|
self
|
27
41
|
end
|
28
42
|
|
29
|
-
def
|
30
|
-
|
43
|
+
def group(id)
|
44
|
+
Group.new self, id
|
45
|
+
end
|
46
|
+
|
47
|
+
def call(*args, &block)
|
48
|
+
call_with_id DEFAULT_GROUP, *args, &block
|
49
|
+
end
|
50
|
+
alias_method :[], :call
|
51
|
+
|
52
|
+
def call_with_id(id, *args, &block)
|
53
|
+
args << block if block
|
54
|
+
thread = nil
|
31
55
|
exclusively do
|
32
|
-
thread
|
33
|
-
@flush
|
34
|
-
|
35
|
-
|
56
|
+
thread = @timeouts[id] ||= new_thread { begin_delay id, args }
|
57
|
+
@flush = [id]
|
58
|
+
old_args = thread[:args]
|
59
|
+
thread[:args] =
|
60
|
+
if @reducer
|
61
|
+
initial, reducer = @reducer
|
62
|
+
old_args ||= initial || []
|
63
|
+
if reducer.is_a? Symbol
|
64
|
+
old_args.__send__ reducer, args
|
65
|
+
elsif reducer.respond_to? :call
|
66
|
+
reducer.call old_args, args, id
|
67
|
+
end
|
68
|
+
else
|
69
|
+
args.empty? ? old_args : args
|
70
|
+
end
|
71
|
+
if @flush == true
|
36
72
|
thread.kill
|
37
73
|
@timeouts.delete id
|
38
74
|
@threads.delete thread
|
39
75
|
@flush = false
|
40
76
|
else
|
41
|
-
thread[:args] = args
|
42
77
|
thread[:run_at] = Time.now + @delay
|
43
78
|
end
|
44
79
|
end or
|
45
|
-
|
80
|
+
run_block thread
|
46
81
|
self
|
47
82
|
end
|
48
83
|
|
49
|
-
def flush(
|
50
|
-
if
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
end
|
84
|
+
def flush(id = EMPTY)
|
85
|
+
if @lock.owned?
|
86
|
+
raise ArgumentError, 'You cannot flush other groups from inside a reducer' unless id == EMPTY || [id] == @flush
|
87
|
+
@flush = true
|
88
|
+
elsif id == EMPTY
|
89
|
+
flush @timeouts.keys.first while @timeouts.any?
|
56
90
|
else
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
dead = exclusively do
|
62
|
-
if (thread = @timeouts.delete(id))
|
63
|
-
thread.kill
|
64
|
-
@threads.delete thread
|
65
|
-
end
|
91
|
+
dead = exclusively do
|
92
|
+
if (thread = @timeouts.delete(id))
|
93
|
+
thread.kill
|
94
|
+
@threads.delete thread
|
66
95
|
end
|
67
|
-
dead[:block].call *dead[:args] if dead
|
68
96
|
end
|
97
|
+
run_block dead if dead
|
69
98
|
end
|
70
99
|
self
|
71
100
|
end
|
72
101
|
|
73
|
-
def join(
|
74
|
-
|
102
|
+
def join(id = EMPTY, kill_first: false)
|
103
|
+
if id == EMPTY
|
104
|
+
while (thread = exclusively { @threads.find &:alive? })
|
105
|
+
thread.kill if kill_first
|
106
|
+
thread.join
|
107
|
+
end
|
108
|
+
exclusively { [@threads, @timeouts].each &:clear } if kill_first
|
109
|
+
elsif (thread = exclusively { @timeouts.delete id })
|
110
|
+
@threads.delete thread
|
75
111
|
thread.kill if kill_first
|
76
112
|
thread.join
|
77
113
|
end
|
78
|
-
exclusively { [@threads, @timeouts].each &:clear } if kill_first
|
79
114
|
self
|
80
115
|
end
|
81
116
|
|
82
|
-
def kill
|
83
|
-
join true
|
117
|
+
def kill(id = EMPTY)
|
118
|
+
join id, kill_first: true
|
84
119
|
end
|
85
120
|
|
86
|
-
def
|
87
|
-
|
121
|
+
def inspect_params
|
122
|
+
{delay: @delay, timeouts: @timeouts.count, threads: @threads.count}
|
123
|
+
end
|
124
|
+
|
125
|
+
def to_proc
|
126
|
+
method(:call).to_proc
|
127
|
+
end
|
128
|
+
|
129
|
+
def sleeping?
|
130
|
+
@timeouts.length.nonzero?
|
131
|
+
end
|
132
|
+
|
133
|
+
def runs_at(id = DEFAULT_GROUP)
|
134
|
+
thread = @timeouts[id]
|
135
|
+
thread && thread[:run_at]
|
88
136
|
end
|
89
137
|
|
90
138
|
private
|
91
139
|
|
92
|
-
def begin_delay(id,
|
93
|
-
thread[:
|
140
|
+
def begin_delay(id, args)
|
141
|
+
thread[:run_at] = Time.now + @delay
|
142
|
+
thread[:args] ||= args
|
94
143
|
sleep @delay
|
95
144
|
until exclusively { (thread[:run_at] <= Time.now).tap { |ready| @timeouts.delete id if ready } }
|
96
145
|
sleep [thread[:run_at] - Time.now, 0].max
|
97
146
|
end
|
98
|
-
|
147
|
+
run_block thread
|
99
148
|
rescue => ex
|
100
149
|
@timeouts.reject! { |_, v| v == thread }
|
101
150
|
(rescuer = @rescuers.find { |klass, _| ex.is_a? klass }) && rescuer.last[ex]
|
@@ -103,14 +152,8 @@ class Debouncer
|
|
103
152
|
exclusively { @threads.delete thread }
|
104
153
|
end
|
105
154
|
|
106
|
-
def
|
107
|
-
|
108
|
-
if @reducer
|
109
|
-
initial, reducer = @reducer
|
110
|
-
reducer[old_args || initial || [], new_args, id]
|
111
|
-
else
|
112
|
-
new_args.empty? ? old_args : new_args
|
113
|
-
end
|
155
|
+
def run_block(thread)
|
156
|
+
@block.call *thread[:args]
|
114
157
|
end
|
115
158
|
|
116
159
|
def new_thread(*args, &block)
|
@@ -1,74 +1,50 @@
|
|
1
|
-
require 'debouncer'
|
2
|
-
|
3
1
|
class Debouncer
|
4
2
|
module Debounceable
|
5
|
-
|
3
|
+
SUFFIXES = {
|
4
|
+
'?' => '_predicate',
|
5
|
+
'!' => '_dangerous',
|
6
|
+
'=' => '_assignment'
|
7
|
+
}
|
8
|
+
def debounce(name, delay, rescue_with: nil, grouped: false, reduce_with: nil, class_method: false)
|
6
9
|
name =~ /^(\w+)([?!=]?)$/ or
|
7
10
|
raise ArgumentError, 'Invalid method name'
|
8
11
|
|
9
12
|
base_name = $1
|
10
13
|
suffix = $2
|
11
14
|
immediate = "#{base_name}_immediately#{suffix}"
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
when Symbol
|
17
|
-
__send__ rescue_with, ex
|
18
|
-
when Proc
|
19
|
-
rescue_with[ex]
|
20
|
-
else
|
21
|
-
# Silent failure
|
22
|
-
end
|
23
|
-
end if rescue_with
|
24
|
-
|
25
|
-
d.reducer do |old, args, id|
|
26
|
-
case reduce_with
|
27
|
-
when Symbol
|
28
|
-
debouncing_instance(name, id).__send__ reduce_with, old, args
|
29
|
-
when Proc
|
30
|
-
reduce_with[old, args, id]
|
31
|
-
else
|
32
|
-
raise ArgumentError
|
33
|
-
end
|
34
|
-
end if reduce_with
|
35
|
-
end
|
15
|
+
debouncer = "@#{base_name}#{SUFFIXES[suffix]}_debouncer"
|
16
|
+
extras = ''
|
17
|
+
extras << ".reducer { |old, new| self.#{reduce_with} old, new }" if reduce_with
|
18
|
+
extras << ".rescuer { |ex| self.#{rescue_with} ex }" if rescue_with
|
36
19
|
|
37
20
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
21
|
+
#{'class << self' if class_method}
|
22
|
+
|
38
23
|
alias_method :#{immediate}, :#{name}
|
39
24
|
|
40
|
-
def #{name}(*args)
|
41
|
-
|
42
|
-
#{
|
43
|
-
#{self.name}.debouncer_for_method(:#{name}).debounce(id, *args) { |*args| #{immediate} *args }
|
25
|
+
def #{name}(*args, &block)
|
26
|
+
#{debouncer} ||= ::Debouncer.new(#{delay}) { |*args| self.#{immediate} *args }#{extras}
|
27
|
+
#{debouncer}#{'.group(args.first)' if grouped}.call *args, &block
|
44
28
|
end
|
45
29
|
|
46
|
-
def flush_#{name}
|
47
|
-
#{
|
30
|
+
def flush_#{name}(*args)
|
31
|
+
#{debouncer}.flush *args if #{debouncer}
|
48
32
|
end
|
49
33
|
|
50
|
-
def
|
51
|
-
|
34
|
+
def join_#{name}(*args)
|
35
|
+
#{debouncer}.join *args if #{debouncer}
|
52
36
|
end
|
53
37
|
|
54
|
-
def
|
55
|
-
|
38
|
+
def cancel_#{name}(*args)
|
39
|
+
#{debouncer}.kill *args if #{debouncer}
|
56
40
|
end
|
57
|
-
RUBY
|
58
|
-
end
|
59
41
|
|
60
|
-
|
61
|
-
|
62
|
-
@method_debouncers[name] ||= Debouncer.new(delay, &block)
|
42
|
+
#{'end' if class_method}
|
43
|
+
RUBY
|
63
44
|
end
|
64
45
|
|
65
|
-
def
|
66
|
-
|
67
|
-
if instance
|
68
|
-
hash[id] = instance
|
69
|
-
else
|
70
|
-
hash[id]
|
71
|
-
end
|
46
|
+
def mdebounce(name, delay, **opts)
|
47
|
+
debounce name, delay, class_method: true, **opts
|
72
48
|
end
|
73
49
|
end
|
74
50
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class Debouncer
|
2
|
+
class Group
|
3
|
+
include Inspection
|
4
|
+
|
5
|
+
attr_reader :id, :debouncer
|
6
|
+
|
7
|
+
def initialize(debouncer, id)
|
8
|
+
@debouncer = debouncer
|
9
|
+
@id = id
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(*args, &block)
|
13
|
+
@debouncer.call_with_id @id, *args, &block
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_proc
|
17
|
+
method(:call).to_proc
|
18
|
+
end
|
19
|
+
|
20
|
+
def flush
|
21
|
+
@debouncer.flush @id
|
22
|
+
end
|
23
|
+
|
24
|
+
def join
|
25
|
+
@debouncer.join @id
|
26
|
+
end
|
27
|
+
|
28
|
+
def kill
|
29
|
+
@debouncer.kill @id
|
30
|
+
end
|
31
|
+
|
32
|
+
def inspect_params
|
33
|
+
{delay: @debouncer.delay, scheduled: @debouncer.runs_at(@id) || 'idle'}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/debouncer/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: debouncer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Neil E. Pearson
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-02-
|
11
|
+
date: 2017-02-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -73,6 +73,8 @@ files:
|
|
73
73
|
- debouncer.gemspec
|
74
74
|
- lib/debouncer.rb
|
75
75
|
- lib/debouncer/debounceable.rb
|
76
|
+
- lib/debouncer/group.rb
|
77
|
+
- lib/debouncer/inspection.rb
|
76
78
|
- lib/debouncer/version.rb
|
77
79
|
homepage: https://github.com/hx/debouncer
|
78
80
|
licenses:
|