Created
December 23, 2021 18:52
-
-
Save disintegrator/be65e25723f836c1048175b74d127e73 to your computer and use it in GitHub Desktop.
A debouncer that sends pulses on a channel (Golang, Go)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package debounce | |
import ( | |
"context" | |
"errors" | |
"fmt" | |
"sync" | |
"time" | |
) | |
// stand-in for something better e.g. github.com/pkg/errors | |
type debouncerError struct { | |
current error | |
parent error | |
} | |
func (e *debouncerError) Error() string { | |
if e.parent == nil { | |
return e.current.Error() | |
} | |
return fmt.Sprintf("%s: %s", e.parent.Error(), e.current.Error()) | |
} | |
func (e *debouncerError) Is(target error) bool { | |
isParent := false | |
if e.parent != nil { | |
isParent = target == e.parent | |
} | |
return target == e.current || isParent | |
} | |
var ( | |
ErrDebouncerClosed = errors.New("debouncer is no longer running") | |
) | |
type DebounceOptions struct { | |
Period time.Duration | |
} | |
func Channel(ctx context.Context, options *DebounceOptions) (debouncer func() error, pulseC <-chan struct{}) { | |
var mut sync.Mutex | |
var timer *time.Timer | |
pulse := make(chan struct{}, 1) | |
pulseC = pulse | |
debouncer = func() error { | |
mut.Lock() | |
defer mut.Unlock() | |
if timer != nil { | |
timer.Stop() | |
} | |
select { | |
case <-ctx.Done(): | |
return &debouncerError{current: ErrDebouncerClosed, parent: ctx.Err()} | |
default: | |
} | |
timer = time.AfterFunc(options.Period, func() { | |
select { | |
case pulse <- struct{}{}: | |
default: | |
// If we are unable to write a value then we assume there is either: | |
// - No receiver. In which case, drop the pulses since there is already | |
// one buffered. | |
// - Slow receiver. In which case, drop the pulses since they will | |
// eventually pick up the buffered pulse. | |
} | |
}) | |
return nil | |
} | |
go func() { | |
<-ctx.Done() | |
mut.Lock() | |
defer mut.Unlock() | |
if timer != nil { | |
timer.Stop() | |
} | |
}() | |
return | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package debounce | |
import ( | |
"context" | |
"errors" | |
"testing" | |
"time" | |
) | |
func TestChannel(t *testing.T) { | |
debouncer, pulseC := Channel(context.Background(), &DebounceOptions{ | |
Period: 50 * time.Millisecond, | |
}) | |
for i := 0; i < 10; i++ { | |
time.Sleep(25 * time.Millisecond) | |
if err := debouncer(); err != nil { | |
t.Fatalf("unexpected error: %s", err) | |
} | |
} | |
select { | |
case <-pulseC: | |
t.Fatal("did not expect to receive on pulse channel") | |
default: | |
// noop | |
} | |
time.Sleep(60 * time.Millisecond) | |
select { | |
case <-pulseC: | |
// noop | |
default: | |
t.Fatal("expected to receive on pulse channel") | |
} | |
} | |
func TestChannel_NoActivity(t *testing.T) { | |
_, pulseC := Channel(context.Background(), &DebounceOptions{ | |
Period: 50 * time.Millisecond, | |
}) | |
time.Sleep(60 * time.Millisecond) | |
select { | |
case <-pulseC: | |
t.Fatal("did not expect to receive on pulse channel") | |
default: | |
// noop | |
} | |
} | |
func TestChannel_Closed(t *testing.T) { | |
ctx, cancel := context.WithCancel(context.Background()) | |
debouncer, pulseC := Channel(ctx, &DebounceOptions{ | |
Period: 50 * time.Millisecond, | |
}) | |
cancel() | |
if err := debouncer(); !errors.Is(err, ErrDebouncerClosed) || !errors.Is(err, context.Canceled) { | |
t.Fatalf("did not get the desired error. got: %s", err) | |
} | |
time.Sleep(60 * time.Millisecond) | |
select { | |
case <-pulseC: | |
t.Fatal("did not expect to receive on pulse channel") | |
default: | |
// noop | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment