Skip to content

Commit

Permalink
Merge pull request #1 from ratanphayade/master
Browse files Browse the repository at this point in the history
Master
  • Loading branch information
ratanphayade authored Oct 15, 2020
2 parents 3f51f2a + e293289 commit b668771
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# ignore IDE files
.idea
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Debouncer

Debounce and throttle are two similar (but different!) techniques to control how many times we allow a function to be
executed over time. Debounce is used when you have to consider only the final state.

A typical example for this is auto submit / auto suggestions. In these cases rather than making a request to server for
suggestions, it's recommended to wait for some time. If the end user is still typing then ignore the previous request
and consider the latest value.

This can be very well adapted in multiple places to reduce the load on the system.

## Getting Started
install `debouncer`
```
$ go get -u -v github.com/ratanphayade/debouncer
```

### Using Debouncer

#### Create an instance
To create an instance
```go
d := debouncer.NewDebouncer(ttl)
```
Here ttl is the interval until which the event has to wait before performing action. In case before ttl another event
received then, previous event will be ignored, and it'll again wait for a duration specified by ttl before performing the
action

*Note: One instance can handle only action. If required to have multiple action then, multiple instance has to be created*

#### Start the action listener
Action should be of type
```go
type Action func(ctx context.Context, value interface{}) error
```

Once we have the action defined, we can start the action listener
```go
d.Do(ctx, func(_ context.Context, val interface{}) error {
counter++
result = result + val.(int)
return nil
})
```

#### Triggering the event
Calling below method with every event will invoke the debounce action
```go
d.TriggerAction(value)
```

91 changes: 91 additions & 0 deletions debouncer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package debouncer

import (
"context"
"sync"
"time"
)

// Action method signature of the action
// which has to be performed on event
type Action func(ctx context.Context, value interface{}) error

type Debouncer struct {
// Input represents the change event
Input chan interface{}

// Interval represent the max time it should wait
// before performing the Action
Interval time.Duration

// once will be used to ensure that the Do method
// is called only once per deboubncer instance
// as a single debounce can take care of only one operation
// calling it multiple times might cause inconsistencies
once sync.Once
}

// NewDebouncer creates a new instance of debouncer
// this will create an unbuffered channel to capture a event
func NewDebouncer(interval time.Duration) *Debouncer {
return &Debouncer{
Input: make(chan interface{}),
Interval: interval,
}
}

// TriggerAction records an event to perform the Action provide
// this will add given value to the input channel as notification
// for debouncer
func (d *Debouncer) TriggerAction(val interface{}) {
d.Input <- val
}

// Do will run the debounce in a go routine
// and it'll make sure that its been invoked only once
// as multiple action can not fall under same config
func (d *Debouncer) Do(ctx context.Context, action Action) {
// ensure debouncing is started only once per instance
d.once.Do(func() {
go d.debounce(ctx, action)
})
}

// debounce will make sure that the given action is not performed repeatedly
// if its triggered multiple times within a given interval
func (d *Debouncer) debounce(ctx context.Context, action Action) {
var (
// hasEvent represents of there is a valid event received
// this will help avoid unnecessary triggering if the method
hasEvent bool

// value holds the latest input received
value interface{}

timer = time.NewTimer(d.Interval)
)

for {
select {
// if there is an event then reset the timer
// and update the hasEvent to true representing
// to trigger the function once the timer ends
case value = <-d.Input:
hasEvent = true
timer.Reset(d.Interval)

// if the timer ends there is a valid event
// then call the Action provider
case <-timer.C:
if hasEvent {
_ = action(ctx, value)
hasEvent = false
}

// if the application is being terminated
// then stop the debouncing
case <-ctx.Done():
return
}
}
}
41 changes: 41 additions & 0 deletions debouncer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package debouncer_test

import (
"context"
"testing"
"time"

"github.com/ratanphayade/debouncer"
"github.com/stretchr/testify/assert"
)

func TestDebounce_Do(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// initialize the debouncer with the interval
// until which the trigger will wait before executing the action
d := debouncer.NewDebouncer(100 * time.Millisecond)
var (
counter int
result int
)

// start the action listener
d.Do(ctx, func(_ context.Context, val interface{}) error {
counter++
result = result + val.(int)
return nil
})

// triggering multiple action events
for i := 0; i < 5; i++ {
d.TriggerAction(i)
}

time.Sleep(500 * time.Millisecond)
cancel()

assert.Equal(t, 1, counter)
assert.Equal(t, 4, result)
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/ratanphayade/debouncer

go 1.13

require github.com/stretchr/testify v1.6.1
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 comments on commit b668771

Please sign in to comment.