diff --git a/.gitignore b/.gitignore index 66fd13c..d1baad6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ # Dependency directories (remove the comment below to include it) # vendor/ +.idea \ No newline at end of file diff --git a/debouncer.go b/debouncer.go index 2d1abcc..b59e8bb 100644 --- a/debouncer.go +++ b/debouncer.go @@ -11,11 +11,68 @@ type Debouncer struct { triggeredFunc func() mu sync.Mutex done chan struct{} + options Options } +type Options struct { + Leading, Trailing bool +} + +type DebouncerOptions func(*Debouncer) + +type DebouncerType string + +const ( + TRAILING DebouncerType = "trailing" + LEADING = "leading" + OVERLAPPED = "overlapped" + INACTIVE = "inactive" +) // New creates a new instance of debouncer. Each instance of debouncer works independent, concurrency with different wait duration. func New(duration time.Duration) *Debouncer { - return &Debouncer{timeDuration: duration, triggeredFunc: func() {}} + return &Debouncer{timeDuration: duration, triggeredFunc: func() {}, options: Options{false, true}} +} + +func NewWithOptions(opts ...DebouncerOptions) *Debouncer { + var ( + defaultDuration = 1 * time.Minute + defaultOptions = Options{false, true} + defaultTriggeredFunc = func() {} + ) + + d := &Debouncer{ + timeDuration: defaultDuration, + triggeredFunc: defaultTriggeredFunc, + options: defaultOptions, + done: make(chan struct{}), + } + + for _, opt := range opts { + opt(d) + } + + return d +} + +// WithOptions sets the options of debouncer instance. +func WithOptions(options Options) DebouncerOptions { + return func(d *Debouncer) { + d.options = options + } +} + +// WithTriggered sets the triggered function of debouncer instance. +func WithTriggered(triggeredFunc func()) DebouncerOptions { + return func(d *Debouncer) { + d.triggeredFunc = triggeredFunc + } +} + +// WithTimeDuration sets the time duration of debouncer instance. +func WithTimeDuration(timeDuration time.Duration) DebouncerOptions { + return func(d *Debouncer) { + d.timeDuration = timeDuration + } } // WithTriggered attached a triggered function to debouncer instance and return the same instance of debouncer to use. @@ -29,14 +86,45 @@ func (d *Debouncer) SendSignal() { d.mu.Lock() defer d.mu.Unlock() - d.Cancel() - d.timer = time.AfterFunc(d.timeDuration, func() { + switch d.getDebounceType() { + case TRAILING: + d.Cancel() + d.timer = d.invokeTriggeredFunc() + case LEADING: + if d.timer != nil { + d.timer = d.invokeTriggeredFunc() + } + case OVERLAPPED: + d.timer = d.invokeTriggeredFunc() + default: + } + +} + +func (d *Debouncer) invokeTriggeredFunc() *time.Timer { + return time.AfterFunc(d.timeDuration, func() { d.triggeredFunc() close(d.done) d.done = make(chan struct{}) }) } +func (d *Debouncer) getDebounceType() DebouncerType { + if !d.options.Leading && d.options.Trailing { + return TRAILING + } + + if d.options.Leading && !d.options.Trailing { + return LEADING + } + + if d.options.Leading && d.options.Trailing { + return OVERLAPPED + } + + return INACTIVE +} + // Do run the signalFunc() and call SendSignal() after all. The signalFunc() and SendSignal() function run sequentially. func (d *Debouncer) Do(signalFunc func()) { signalFunc() diff --git a/debouncer_test.go b/debouncer_test.go index 113c489..4a7c341 100644 --- a/debouncer_test.go +++ b/debouncer_test.go @@ -9,21 +9,20 @@ import ( ) func Example() { - wait := 5 * time.Second - debouncer := godebouncer.New(wait).WithTriggered(func() { - fmt.Println("Trigger") // Triggered func will be called after 5 seconds from last SendSignal(). - }) - - fmt.Println("Action 1") + duration := 5 * time.Second + debouncer := godebouncer.NewWithOptions( + godebouncer.WithTimeDuration(duration), + godebouncer.WithTriggered(func() { + // Triggered func will be called after 5 seconds from last SendSignal()/Do(). + fmt.Println("Trigger") + }), + ) debouncer.SendSignal() - time.Sleep(1 * time.Second) - - fmt.Println("Action 2") debouncer.SendSignal() // After 5 seconds, the trigger will be called. - //Previous `SendSignal()` will be ignore to trigger the triggered function. + // Previous `SendSignal()` will be ignored to trigger the triggered function. <-debouncer.Done() } @@ -211,6 +210,34 @@ func TestDebounceUpdateDurationAfterSendSignal(t *testing.T) { } } +// Test Debounce Leading +func TestDebounceLeading(t *testing.T) { + countPtr, incrementCount := createIncrementCount(0) + debouncer := godebouncer.NewWithOptions( + godebouncer.WithTimeDuration(300*time.Millisecond), + godebouncer.WithTriggered(incrementCount), + godebouncer.WithOptions(godebouncer.Options{Leading: true, Trailing: false}), + ) + + expectedCounter := 1 + + debouncer.SendSignal() + debouncer.SendSignal() + //debouncer.SendSignal() + <-debouncer.Done() + //time.Sleep(300 * time.Millisecond) + //debouncer.SendSignal() + // + //debouncer.SendSignal() + //debouncer.SendSignal() + // + //<-debouncer.Done() + + if *countPtr != expectedCounter { + t.Errorf("Expected count %d, was %d", expectedCounter, *countPtr) + } +} + func TestDone(t *testing.T) { countPtr, incrementCount := createIncrementCount(0) debouncer := godebouncer.New(200 * time.Millisecond).WithTriggered(incrementCount)