Skip to content

Commit

Permalink
Simplify implementation using time.AfterFunc
Browse files Browse the repository at this point in the history
Closes #1
  • Loading branch information
bep committed Feb 2, 2019
1 parent a8044c0 commit dfd260a
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 146 deletions.
30 changes: 2 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,7 @@
[![codecov](https://codecov.io/gh/bep/debounce/branch/master/graph/badge.svg)](https://codecov.io/gh/bep/debounce)
[![Release](https://img.shields.io/github/release/bep/debounce.svg?style=flat-square)](https://github.com/bep/debounce/releases/latest)

## Why?

This may seem like a fairly narrow library, so why not copy-and-paste it when needed? Sure -- but this is, however, slightly more usable than [left-pad](https://www.npmjs.com/package/left-pad), and as I move my client code into the [GopherJS](https://github.com/gopherjs/gopherjs) world, a [debounce](https://davidwalsh.name/javascript-debounce-function) function is a must-have.

This library works, but if you find any issue or a potential improvement, please create an issue or a pull request!

## Use

This package provides a debouncer func. The most typical use case would be the user
typing a text into a form; the UI needs an update, but let's wait for a break.

`New` returns a debounced function and a channel that can be closed to signal a stop of the goroutine. The function will, as long as it continues to be invoked, not be triggered. The function will be called after it stops being called for the given duration. Note that a stop signal means a full stop of the debouncer; there is no concept of flushing future invocations.

**Note:** The created debounced function can be invoked with different functions, if needed, the last one will win.

An example:
## Example

```go
func ExampleNew() {
Expand All @@ -31,7 +16,7 @@ func ExampleNew() {
atomic.AddUint64(&counter, 1)
}

debounced, finish, done := debounce.New(100 * time.Millisecond)
debounced := debounce.New(100 * time.Millisecond)

for i := 0; i < 3; i++ {
for j := 0; j < 10; j++ {
Expand All @@ -41,21 +26,10 @@ func ExampleNew() {
time.Sleep(200 * time.Millisecond)
}

close(finish)

<-done

c := int(atomic.LoadUint64(&counter))

fmt.Println("Counter is", c)
// Output: Counter is 3
}
```

## Tests

To run the tests, you need to install `Leaktest`:

```bash
go get -u github.com/fortytw2/leaktest
```
67 changes: 17 additions & 50 deletions debounce.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,60 +12,27 @@ import (
"time"
)

// New returns a debounced function and two channels:
// 1. A quit channel that can be closed to signal a stop
// 2. A done channel that signals when the debouncer is completed
// of the goroutine.
// The function will, as long as it continues to be invoked, not be triggered.
// The function will be called after it stops being called for the given duration.
// The created debounced function can be invoked with different functions, if needed,
// New returns a debounced function that takes another functions as its argument.
// This function will be called when the debounced function stops being called
// for the given duration.
// The debounced function can be invoked with different functions, if needed,
// the last one will win.
// Also note that a stop signal means a full stop of the debouncer; there is no
// concept of flushing future invocations.
func New(d time.Duration) (func(f func()), chan struct{}, chan struct{}) {
in, out, quit := debounceChan(d)
done := make(chan struct{})
func New(after time.Duration) func(f func()) {
d := &debouncer{after: after}

go func() {
for {
select {
case f := <-out:
f()
case <-quit:
close(out)
close(in)
close(done)
return
}
}
}()

debounce := func(f func()) {
in <- f
return func(f func()) {
d.add(f)
}

return debounce, quit, done
}

func debounceChan(interval time.Duration) (in, out chan func(), quit chan struct{}) {
in = make(chan func(), 1)
out = make(chan func())
quit = make(chan struct{})

go func() {
var f func() = func() {}
for {
select {
case f = <-in:
case <-time.After(interval):
out <- f
<-in
// new interval
case <-quit:
return
}
}
}()
type debouncer struct {
after time.Duration
timer *time.Timer
}

return
func (d *debouncer) add(f func()) {
if d.timer != nil {
d.timer.Stop()
}
d.timer = time.AfterFunc(d.after, f)
}
85 changes: 17 additions & 68 deletions debounce_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@ package debounce_test

import (
"fmt"
"sync"
"sync/atomic"
"testing"
"time"

"github.com/bep/debounce"
"github.com/fortytw2/leaktest"
)

func TestDebounce(t *testing.T) {
defer leaktest.Check(t)()

var (
counter1 uint64
counter2 uint64
Expand All @@ -31,7 +27,7 @@ func TestDebounce(t *testing.T) {
atomic.AddUint64(&counter2, 2)
}

debounced, shutdown, done := debounce.New(100 * time.Millisecond)
debounced := debounce.New(100 * time.Millisecond)

for i := 0; i < 3; i++ {
for j := 0; j < 10; j++ {
Expand All @@ -52,10 +48,6 @@ func TestDebounce(t *testing.T) {
time.Sleep(200 * time.Millisecond)
}

close(shutdown)

<-done

c1 := int(atomic.LoadUint64(&counter1))
c2 := int(atomic.LoadUint64(&counter2))
if c1 != 3 {
Expand All @@ -66,65 +58,28 @@ func TestDebounce(t *testing.T) {
}
}

func TestDebounceInParallel(t *testing.T) {
defer leaktest.Check(t)()

var counter uint64

f := func() {
atomic.AddUint64(&counter, 1)
}

debounced, shutdown, done := debounce.New(100 * time.Millisecond)

var wg sync.WaitGroup

for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
debouncedInner, shutdown, done := debounce.New(100 * time.Millisecond)
for j := 0; j < 10; j++ {
debouncedInner(f)
debounced(f)
}
time.Sleep(150 * time.Millisecond)
close(shutdown)
<-done
}()
}
wg.Wait()

close(shutdown)
// Issue #1
func TestDebounceDelayed(t *testing.T) {

<-done

c := int(atomic.LoadUint64(&counter))
if c != 21 {
t.Error("Expected count 21, was", c)
}
}

func TestDebounceCloseEarly(t *testing.T) {
defer leaktest.Check(t)()

var counter uint64
var (
counter1 uint64
)

f := func() {
atomic.AddUint64(&counter, 1)
f1 := func() {
atomic.AddUint64(&counter1, 1)
}

debounced, finish, done := debounce.New(100 * time.Millisecond)
debounced := debounce.New(100 * time.Millisecond)

debounced(f)
time.Sleep(110 * time.Millisecond)

close(finish)
debounced(f1)

<-done
time.Sleep(110 * time.Millisecond)

c := int(atomic.LoadUint64(&counter))
if c != 0 {
t.Error("Expected count 0, was", c)
c1 := int(atomic.LoadUint64(&counter1))
if c1 != 1 {
t.Error("Expected count 1, was", c1)
}

}
Expand All @@ -136,14 +91,12 @@ func BenchmarkDebounce(b *testing.B) {
atomic.AddUint64(&counter, 1)
}

debounced, finish, done := debounce.New(100 * time.Millisecond)
debounced := debounce.New(100 * time.Millisecond)

b.ResetTimer()
for i := 0; i < b.N; i++ {
debounced(f)
}
close(finish)
<-done

c := int(atomic.LoadUint64(&counter))
if c != 0 {
Expand All @@ -158,7 +111,7 @@ func ExampleNew() {
atomic.AddUint64(&counter, 1)
}

debounced, finish, done := debounce.New(100 * time.Millisecond)
debounced := debounce.New(100 * time.Millisecond)

for i := 0; i < 3; i++ {
for j := 0; j < 10; j++ {
Expand All @@ -168,10 +121,6 @@ func ExampleNew() {
time.Sleep(200 * time.Millisecond)
}

close(finish)

<-done

c := int(atomic.LoadUint64(&counter))

fmt.Println("Counter is", c)
Expand Down
Empty file added go.sum
Empty file.

0 comments on commit dfd260a

Please sign in to comment.