From 5c3cb2a5b7a7d5812af2db5b9b5b983cd3fb14cd Mon Sep 17 00:00:00 2001 From: ratanphayade Date: Fri, 16 Oct 2020 02:27:55 +0530 Subject: [PATCH 1/5] git: updates ignore file list --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf33604 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# ignore IDE files +.idea From 17fc301c691e6dac6ef4e386bbfc1b8c3aa63c6a Mon Sep 17 00:00:00 2001 From: ratanphayade Date: Fri, 16 Oct 2020 02:28:20 +0530 Subject: [PATCH 2/5] debouncer: package structure with the event lister added --- debouncer.go | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 debouncer.go diff --git a/debouncer.go b/debouncer.go new file mode 100644 index 0000000..f25cb09 --- /dev/null +++ b/debouncer.go @@ -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 + } + } +} From 28cc659dea06f0f711f6a2c7332600debdb6f11b Mon Sep 17 00:00:00 2001 From: ratanphayade Date: Fri, 16 Oct 2020 02:28:33 +0530 Subject: [PATCH 3/5] test: adds unit test cases --- debouncer_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 debouncer_test.go diff --git a/debouncer_test.go b/debouncer_test.go new file mode 100644 index 0000000..4c0c2c0 --- /dev/null +++ b/debouncer_test.go @@ -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) +} From 2b4d6091c6923ea86f5518854ebe3d9249a0b7fc Mon Sep 17 00:00:00 2001 From: ratanphayade Date: Fri, 16 Oct 2020 02:29:06 +0530 Subject: [PATCH 4/5] mod: application dependecies added --- go.mod | 5 +++++ go.sum | 10 ++++++++++ 2 files changed, 15 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d809b12 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/ratanphayade/debouncer + +go 1.13 + +require github.com/stretchr/testify v1.6.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..56d62e7 --- /dev/null +++ b/go.sum @@ -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= From e2932894d527ea0e1c9beecb5aaa7020bf66dc88 Mon Sep 17 00:00:00 2001 From: ratanphayade Date: Fri, 16 Oct 2020 02:29:36 +0530 Subject: [PATCH 5/5] documentaion: README udpated with usage guide --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..b4d5937 --- /dev/null +++ b/README.md @@ -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) +``` +