Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions debounce.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ func New(after time.Duration) func(f func()) {
}
}

// NewWithCancel returns a debounced function together with a cancel function.
// The debounced function behaves like the one returned by New: it takes another
// function as its argument, and that function will be invoked when calls to the
// debounced function have stopped for the given duration. If invoked multiple
// times, the last provided function will win.
//
// The returned cancel function stops any pending timer and prevents the
// currently scheduled function (if any) from being called. Calling cancel has
// no effect if no function is scheduled or if it already executed.
//
// This is useful in shutdown scenarios where the final scheduled function must
// be suppressed or handled explicitly.
func NewWithCancel(after time.Duration) (func(f func()), func()) {
d := &debouncer{after: after}

return func(f func()) {
d.add(f)
}, d.cancel
}

type debouncer struct {
mu sync.Mutex
after time.Duration
Expand All @@ -41,3 +61,13 @@ func (d *debouncer) add(f func()) {
}
d.timer = time.AfterFunc(d.after, f)
}

func (d *debouncer) cancel() {
d.mu.Lock()
defer d.mu.Unlock()

if d.timer != nil {
d.timer.Stop()
d.timer = nil
}
}
25 changes: 25 additions & 0 deletions debounce_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,28 @@ func ExampleNew() {
fmt.Println("Counter is", c)
// Output: Counter is 3
}

func TestDebounceCancel(t *testing.T) {
var called int32

debounced, cancel := debounce.NewWithCancel(50 * time.Millisecond)

// Schedule a call that would normally be executed.
debounced(func() {
atomic.StoreInt32(&called, 1)
})

// Cancel it before the timer is triggered.
cancel()

// Wait slightly longer than the debounce interval - if cancel did not work,
//the function will execute and the test will fail.
time.Sleep(70 * time.Millisecond)

if atomic.LoadInt32(&called) != 0 {
t.Fatal("expected debounced function NOT to be called after cancel")
}

// Additionally, verify that calling cancel repeatedly is safe.
cancel()
}
Loading