├── go.sum ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── go.mod ├── .gitignore ├── LICENSE ├── README.md ├── debounce.go └── debounce_test.go /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | 2 | github: [bep] 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bep/debounce 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | cover.out 27 | nohup.out 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Bjørn Erik Pedersen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Debounce 2 | 3 | [![Tests on Linux, MacOS and Windows](https://github.com/bep/debounce/workflows/Test/badge.svg)](https://github.com/bep/debounce/actions?query=workflow:Test) 4 | [![GoDoc](https://godoc.org/github.com/bep/debounce?status.svg)](https://godoc.org/github.com/bep/debounce) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/bep/debounce)](https://goreportcard.com/report/github.com/bep/debounce) 6 | [![codecov](https://codecov.io/gh/bep/debounce/branch/master/graph/badge.svg)](https://codecov.io/gh/bep/debounce) 7 | [![Release](https://img.shields.io/github/release/bep/debounce.svg?style=flat-square)](https://github.com/bep/debounce/releases/latest) 8 | 9 | ## Example 10 | 11 | ```go 12 | func ExampleNew() { 13 | var counter uint64 14 | 15 | f := func() { 16 | atomic.AddUint64(&counter, 1) 17 | } 18 | 19 | debounced := debounce.New(100 * time.Millisecond) 20 | 21 | for i := 0; i < 3; i++ { 22 | for j := 0; j < 10; j++ { 23 | debounced(f) 24 | } 25 | 26 | time.Sleep(200 * time.Millisecond) 27 | } 28 | 29 | c := int(atomic.LoadUint64(&counter)) 30 | 31 | fmt.Println("Counter is", c) 32 | // Output: Counter is 3 33 | } 34 | ``` 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ master ] 4 | pull_request: 5 | workflow_dispatch: 6 | name: Test 7 | permissions: 8 | contents: read 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | go-version: [1.22.x] 14 | os: [ubuntu-latest, windows-latest] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - name: Install Go 18 | uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 19 | with: 20 | go-version: ${{ matrix.go-version }} 21 | - name: Install staticcheck 22 | run: go install honnef.co/go/tools/cmd/staticcheck@latest 23 | shell: bash 24 | - name: Install golint 25 | run: go install golang.org/x/lint/golint@latest 26 | shell: bash 27 | - name: Update PATH 28 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 29 | shell: bash 30 | - name: Checkout code 31 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 32 | - name: Fmt 33 | if: matrix.os != 'windows-latest' 34 | run: "diff <(gofmt -d .) <(printf '')" 35 | shell: bash 36 | - name: Vet 37 | run: go vet ./... 38 | - name: Staticcheck 39 | run: staticcheck ./... 40 | - name: Lint 41 | run: golint ./... 42 | - name: Test 43 | run: go test -race . 44 | - name: Upload coverage 45 | if: success() && matrix.os == 'ubuntu-latest' 46 | run: | 47 | curl -s https://codecov.io/bash | bash 48 | env: 49 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 50 | shell: bash -------------------------------------------------------------------------------- /debounce.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Bjørn Erik Pedersen . 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | 6 | // Package debounce provides a debouncer func. The most typical use case would be 7 | // the user typing a text into a form; the UI needs an update, but let's wait for 8 | // a break. 9 | package debounce 10 | 11 | import ( 12 | "sync" 13 | "time" 14 | ) 15 | 16 | // New returns a debounced function that takes another functions as its argument. 17 | // This function will be called when the debounced function stops being called 18 | // for the given duration. 19 | // The debounced function can be invoked with different functions, if needed, 20 | // the last one will win. 21 | func New(after time.Duration) func(f func()) { 22 | d := &debouncer{after: after} 23 | 24 | return func(f func()) { 25 | d.add(f) 26 | } 27 | } 28 | 29 | // NewWithCancel returns a debounced function together with a cancel function. 30 | // The debounced function behaves like the one returned by New: it takes another 31 | // function as its argument, and that function will be invoked when calls to the 32 | // debounced function have stopped for the given duration. If invoked multiple 33 | // times, the last provided function will win. 34 | // 35 | // The returned cancel function stops any pending timer and prevents the 36 | // currently scheduled function (if any) from being called. Calling cancel has 37 | // no effect if no function is scheduled or if it already executed. 38 | // 39 | // This is useful in shutdown scenarios where the final scheduled function must 40 | // be suppressed or handled explicitly. 41 | func NewWithCancel(after time.Duration) (func(f func()), func()) { 42 | d := &debouncer{after: after} 43 | 44 | return func(f func()) { 45 | d.add(f) 46 | }, d.cancel 47 | } 48 | 49 | type debouncer struct { 50 | mu sync.Mutex 51 | after time.Duration 52 | timer *time.Timer 53 | } 54 | 55 | func (d *debouncer) add(f func()) { 56 | d.mu.Lock() 57 | defer d.mu.Unlock() 58 | 59 | if d.timer != nil { 60 | d.timer.Stop() 61 | } 62 | d.timer = time.AfterFunc(d.after, f) 63 | } 64 | 65 | func (d *debouncer) cancel() { 66 | d.mu.Lock() 67 | defer d.mu.Unlock() 68 | 69 | if d.timer != nil { 70 | d.timer.Stop() 71 | d.timer = nil 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /debounce_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bjørn Erik Pedersen 2 | // SPDX-License-Identifier: MIT 3 | 4 | package debounce_test 5 | 6 | import ( 7 | "fmt" 8 | "sync" 9 | "sync/atomic" 10 | "testing" 11 | "time" 12 | 13 | "github.com/bep/debounce" 14 | ) 15 | 16 | func TestDebounce(t *testing.T) { 17 | var ( 18 | counter1 uint64 19 | counter2 uint64 20 | ) 21 | 22 | f1 := func() { 23 | atomic.AddUint64(&counter1, 1) 24 | } 25 | 26 | f2 := func() { 27 | atomic.AddUint64(&counter2, 1) 28 | } 29 | 30 | f3 := func() { 31 | atomic.AddUint64(&counter2, 2) 32 | } 33 | 34 | debounced := debounce.New(100 * time.Millisecond) 35 | 36 | for i := 0; i < 3; i++ { 37 | for j := 0; j < 10; j++ { 38 | debounced(f1) 39 | } 40 | 41 | time.Sleep(200 * time.Millisecond) 42 | } 43 | 44 | for i := 0; i < 4; i++ { 45 | for j := 0; j < 10; j++ { 46 | debounced(f2) 47 | } 48 | for j := 0; j < 10; j++ { 49 | debounced(f3) 50 | } 51 | 52 | time.Sleep(200 * time.Millisecond) 53 | } 54 | 55 | c1 := int(atomic.LoadUint64(&counter1)) 56 | c2 := int(atomic.LoadUint64(&counter2)) 57 | if c1 != 3 { 58 | t.Error("Expected count 3, was", c1) 59 | } 60 | if c2 != 8 { 61 | t.Error("Expected count 8, was", c2) 62 | } 63 | } 64 | 65 | func TestDebounceConcurrentAdd(t *testing.T) { 66 | var wg sync.WaitGroup 67 | 68 | var flag uint64 69 | 70 | debounced := debounce.New(100 * time.Millisecond) 71 | 72 | for i := 0; i < 10; i++ { 73 | wg.Add(1) 74 | go func() { 75 | defer wg.Done() 76 | debounced(func() { 77 | atomic.CompareAndSwapUint64(&flag, 0, 1) 78 | }) 79 | }() 80 | } 81 | wg.Wait() 82 | 83 | time.Sleep(500 * time.Millisecond) 84 | c := int(atomic.LoadUint64(&flag)) 85 | if c != 1 { 86 | t.Error("Flag not set") 87 | } 88 | } 89 | 90 | // Issue #1 91 | func TestDebounceDelayed(t *testing.T) { 92 | 93 | var ( 94 | counter1 uint64 95 | ) 96 | 97 | f1 := func() { 98 | atomic.AddUint64(&counter1, 1) 99 | } 100 | 101 | debounced := debounce.New(100 * time.Millisecond) 102 | 103 | time.Sleep(110 * time.Millisecond) 104 | 105 | debounced(f1) 106 | 107 | time.Sleep(200 * time.Millisecond) 108 | 109 | c1 := int(atomic.LoadUint64(&counter1)) 110 | if c1 != 1 { 111 | t.Error("Expected count 1, was", c1) 112 | } 113 | 114 | } 115 | 116 | func BenchmarkDebounce(b *testing.B) { 117 | var counter uint64 118 | 119 | f := func() { 120 | atomic.AddUint64(&counter, 1) 121 | } 122 | 123 | debounced := debounce.New(100 * time.Millisecond) 124 | 125 | b.ResetTimer() 126 | for i := 0; i < b.N; i++ { 127 | debounced(f) 128 | } 129 | 130 | c := int(atomic.LoadUint64(&counter)) 131 | if c != 0 { 132 | b.Fatal("Expected count 0, was", c) 133 | } 134 | } 135 | 136 | func ExampleNew() { 137 | var counter uint64 138 | 139 | f := func() { 140 | atomic.AddUint64(&counter, 1) 141 | } 142 | 143 | debounced := debounce.New(100 * time.Millisecond) 144 | 145 | for i := 0; i < 3; i++ { 146 | for j := 0; j < 10; j++ { 147 | debounced(f) 148 | } 149 | 150 | time.Sleep(200 * time.Millisecond) 151 | } 152 | 153 | c := int(atomic.LoadUint64(&counter)) 154 | 155 | fmt.Println("Counter is", c) 156 | // Output: Counter is 3 157 | } 158 | 159 | func TestDebounceCancel(t *testing.T) { 160 | var called int32 161 | 162 | debounced, cancel := debounce.NewWithCancel(50 * time.Millisecond) 163 | 164 | // Schedule a call that would normally be executed. 165 | debounced(func() { 166 | atomic.StoreInt32(&called, 1) 167 | }) 168 | 169 | // Cancel it before the timer is triggered. 170 | cancel() 171 | 172 | // Wait slightly longer than the debounce interval - if cancel did not work, 173 | //the function will execute and the test will fail. 174 | time.Sleep(70 * time.Millisecond) 175 | 176 | if atomic.LoadInt32(&called) != 0 { 177 | t.Fatal("expected debounced function NOT to be called after cancel") 178 | } 179 | 180 | // Additionally, verify that calling cancel repeatedly is safe. 181 | cancel() 182 | } 183 | --------------------------------------------------------------------------------