From c3a857f8550c9de5e0e49e7cb19a0359a0c65498 Mon Sep 17 00:00:00 2001 From: Oleksii Petrov Date: Sat, 22 Nov 2025 05:33:32 +0200 Subject: [PATCH] implemented cancellable debounce behavior with full test coverage. --- debounce.go | 30 ++++++++++++++++++++++++++++++ debounce_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/debounce.go b/debounce.go index 793d5ed..c73043f 100644 --- a/debounce.go +++ b/debounce.go @@ -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 @@ -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 + } +} diff --git a/debounce_test.go b/debounce_test.go index 3ab2710..2faac9d 100644 --- a/debounce_test.go +++ b/debounce_test.go @@ -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() +}