forked from bep/debounce
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdebounce.go
More file actions
135 lines (111 loc) · 3.33 KB
/
debounce.go
File metadata and controls
135 lines (111 loc) · 3.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// Copyright © 2019 Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>.
// Copyright © 2025 Vsevolod Strukchinsky <floatdrop@gmail.com>.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// Package debounce provides a debouncer func.
package debounce
import (
"math"
"sync"
"time"
)
// Option is a functional option for configuring the debouncer.
type Option func(*debouncer)
const (
NoLimitCalls = math.MaxInt
NoLimitWait = time.Duration(math.MaxInt64)
)
// WithMaxCalls sets the maximum number of calls before the debounced function is executed.
// By default, there is no limit.
func WithMaxCalls(count int) Option {
return func(d *debouncer) {
d.maxCalls = count
}
}
// WithMaxWait sets the maximum wait time before the debounced function is executed.
// This check happens on the debounced function call, so total maximum wait time
// will be (limit + after) if the function is called just before MaxWait limit.
func WithMaxWait(limit time.Duration) Option {
return func(d *debouncer) {
d.maxWait = limit
}
}
// Returns a debounced function. The provided function will be executed
// after a period of inactivity, or when a maximum number of calls or
// time threshold is reached, if configured.
// The debounced function can be invoked with different functions, if needed,
// the last one will win.
func New(after time.Duration, options ...Option) func(fn func()) {
d := &debouncer{
after: after,
startWait: time.Now(),
maxWait: NoLimitWait,
maxCalls: NoLimitCalls,
}
// Creating timer and immediately stop it, so there will be always allocated Timer
d.timer = time.AfterFunc(NoLimitWait, func() {
d.mu.Lock()
if d.calls == 0 {
d.mu.Unlock()
return // MaxCalls or MaxWait reached, call can be dropped
}
d.calls = 0
d.mu.Unlock()
d.fn()
})
d.timer.Stop()
for _, opt := range options {
opt(d)
}
return func(fn func()) {
d.debouncedCall(fn)
}
}
// NewFunc returns a debounced function that always debounces the provided function.
func NewFunc(fn func(), after time.Duration, options ...Option) func() {
debounce := New(after, options...)
return func() {
debounce(fn)
}
}
type debouncer struct {
mu sync.Mutex
after time.Duration
timer *time.Timer
calls int
maxCalls int
startWait time.Time
maxWait time.Duration
// Stores last function to debounce. Will be called after specified duration.
fn func()
}
func (d *debouncer) callLimitReached() bool {
return d.maxCalls != NoLimitCalls && d.calls >= d.maxCalls
}
func (d *debouncer) timeLimitReached() bool {
return d.maxWait != NoLimitWait && time.Since(d.startWait) >= d.maxWait
}
func (d *debouncer) debouncedCall(fn func()) {
d.mu.Lock()
defer d.mu.Unlock()
// Refreshing function reference, so d.timer will call right function
d.fn = fn
// If this is a first call, store startWait time
if d.calls == 0 {
d.startWait = time.Now()
}
// Counting calls
d.calls++
// If the function has been called more than the limit, or if the wait time
// has exceeded the limit, execute the function immediately.
if d.callLimitReached() || d.timeLimitReached() {
d.timer.Stop() // Stop the timer to prevent it from firing later
d.calls = 0
fn := d.fn
go fn() // Execute outside mutex to avoid blocking
} else {
// Restarting timer, if limits were ok
d.timer.Reset(d.after)
}
}