diff --git a/debounce/debounce.go b/debounce/debounce.go new file mode 100644 index 0000000..a6b2454 --- /dev/null +++ b/debounce/debounce.go @@ -0,0 +1,61 @@ +package debounce + +import ( + "sync" + "time" +) + +func NewWithTimeout(after time.Duration, timeout time.Duration) func(f func()) { + d := &debouncer{ + after: after, + timeout: timeout, + } + + return func(f func()) { + d.add(f) + } +} + +type debouncer struct { + mu sync.Mutex + after time.Duration + timeout time.Duration + timer *time.Timer + killTime *time.Timer + queued func() +} + +func (d *debouncer) add(f func()) { + d.mu.Lock() + defer d.mu.Unlock() + + d.queued = f + + if d.timer != nil { + d.timer.Stop() + } + d.timer = time.AfterFunc(d.after, d.execute) + + if d.killTime == nil { + d.killTime = time.AfterFunc(d.timeout, d.execute) + } +} + +func (d *debouncer) execute() { + d.mu.Lock() + defer d.mu.Unlock() + + if d.queued != nil { + d.queued() + } + + if d.timer != nil { + d.timer.Stop() + } + if d.killTime != nil { + d.killTime.Stop() + } + d.timer = nil + d.killTime = nil + d.queued = nil +} diff --git a/debounce/debounce_test.go b/debounce/debounce_test.go new file mode 100644 index 0000000..35e0427 --- /dev/null +++ b/debounce/debounce_test.go @@ -0,0 +1,138 @@ +package debounce + +import ( + "fmt" + "sync" + "sync/atomic" + "testing" + "time" +) + +func TestDebounceWithTimeout(t *testing.T) { + var ( + counter1 uint64 + counter2 uint64 + ) + + f1 := func() { + atomic.AddUint64(&counter1, 1) + } + + f2 := func() { + atomic.AddUint64(&counter2, 1) + } + + debounced := NewWithTimeout(100*time.Millisecond, 300*time.Millisecond) + + for i := 0; i < 3; i++ { + for j := 0; j < 10; j++ { + debounced(f1) + } + + time.Sleep(200 * time.Millisecond) + } + + for i := 0; i < 3; i++ { + for j := 0; j < 10; j++ { + debounced(f2) + } + + time.Sleep(350 * time.Millisecond) + } + + c1 := int(atomic.LoadUint64(&counter1)) + c2 := int(atomic.LoadUint64(&counter2)) + if c1 != 3 { + t.Error("Expected count 3, was", c1) + } + if c2 != 3 { + t.Error("Expected count 3, was", c2) + } +} + +func TestDebounceWithTimeoutConcurrentAdd(t *testing.T) { + var wg sync.WaitGroup + var flag uint64 + + debounced := NewWithTimeout(100*time.Millisecond, 300*time.Millisecond) + + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + debounced(func() { + atomic.CompareAndSwapUint64(&flag, 0, 1) + }) + }() + } + wg.Wait() + + time.Sleep(500 * time.Millisecond) + + c := int(atomic.LoadUint64(&flag)) + if c != 1 { + t.Error("Flag not set") + } +} + +func TestDebounceWithTimeoutDelayed(t *testing.T) { + var counter1 uint64 + + f1 := func() { + atomic.AddUint64(&counter1, 1) + } + debounced := NewWithTimeout(100*time.Millisecond, 300*time.Millisecond) + + time.Sleep(110 * time.Millisecond) + + debounced(f1) + + time.Sleep(200 * time.Millisecond) + + c1 := int(atomic.LoadUint64(&counter1)) + if c1 != 1 { + t.Error("Expected count 1, was", c1) + } +} + +func BenchmarkDebounceWithTimeout(b *testing.B) { + var counter uint64 + + f := func() { + atomic.AddUint64(&counter, 1) + } + + debounced := NewWithTimeout(100*time.Millisecond, 300*time.Millisecond) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + debounced(f) + } + + c := int(atomic.LoadUint64(&counter)) + if c != 0 { + b.Fatal("Expected count 0, was", c) + } +} + +func ExampleNewWithTimeout() { + var counter uint64 + + f := func() { + atomic.AddUint64(&counter, 1) + } + + debounced := NewWithTimeout(100*time.Millisecond, 300*time.Millisecond) + + for i := 0; i < 3; i++ { + for j := 0; j < 10; j++ { + debounced(f) + } + + time.Sleep(200 * time.Millisecond) + } + + c := int(atomic.LoadUint64(&counter)) + + fmt.Println("Counter is", c) +}