Skip to content

Commit

Permalink
feat: adding DebounceBy
Browse files Browse the repository at this point in the history
  • Loading branch information
samber committed Mar 20, 2023
1 parent d8986f4 commit 03b3860
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 10 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

@samber: I sometimes forget to update this file. Ping me on [Twitter](https://twitter.com/samuelberthe) or open an issue in case of error. We need to keep a clear changelog for easier lib upgrade.

## 1.38.0 (xxxx-xx-xx)

Adding:
- lo.ValueOr
- lo.DebounceBy

## 1.37.0 (2022-12-15)

Adding:
Expand Down
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ Supported helpers for slices:
Supported helpers for maps:

- [Keys](#keys)
- [ValueOr](#valueor)
- [Values](#values)
- [PickBy](#pickby)
- [PickByKeys](#pickbykeys)
Expand Down Expand Up @@ -238,6 +239,7 @@ Concurrency helpers:
- [AttemptWithDelay](#attemptwithdelay)
- [AttemptWhileWithDelay](#attemptwhilewithdelay)
- [Debounce](#debounce)
- [DebounceBy](#debounceby)
- [Synchronize](#synchronize)
- [Async](#async)
- [Transaction](#transaction)
Expand Down Expand Up @@ -942,6 +944,20 @@ values := lo.Values[string, int](map[string]int{"foo": 1, "bar": 2})

[[play](https://go.dev/play/p/nnRTQkzQfF6)]

### ValueOr

Creates an array of the map values.

```go
value := lo.ValueOr[string, int](map[string]int{"foo": 1, "bar": 2}, "foo", 42)
// 1

value := lo.ValueOr[string, int](map[string]int{"foo": 1, "bar": 2}, "baz", 42)
// 42
```

[[play](https://go.dev/play/p/bAq9mHErB4V)]

### PickBy

Returns same map type filtered by given predicate.
Expand Down Expand Up @@ -2446,6 +2462,28 @@ cancel()

[[play](https://go.dev/play/p/mz32VMK2nqe)]

### DebounceBy

`NewDebounceBy` creates a debounced instance for each distinct key, that delays invoking functions given until after wait milliseconds have elapsed, until `cancel` is called.

```go
f := func(key string, count int) {
println(key + ": Called once after 100ms when debounce stopped invoking!")
}

debounce, cancel := lo.NewDebounceBy(100 * time.Millisecond, f)
for j := 0; j < 10; j++ {
debounce("first key")
debounce("second key")
}

time.Sleep(1 * time.Second)
cancel("first key")
cancel("second key")
```

[[play](https://go.dev/play/p/d3Vpt6pxhY8)]

### Synchronize

Wraps the underlying callback in a mutex. It receives an optional mutex.
Expand Down Expand Up @@ -2838,6 +2876,8 @@ ok github.com/samber/lo 6.657s

Don't hesitate ;)

Helper naming: helpers must be self explanatory and respect standards (other languages, libraries...). Feel free to suggest many names in your contributions.

### With Docker

```bash
Expand Down
86 changes: 83 additions & 3 deletions retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ type debounce struct {
callbacks []func()
}

func (d *debounce) reset() *debounce {
func (d *debounce) reset() {
d.mu.Lock()
defer d.mu.Unlock()

if d.done {
return d
return
}

if d.timer != nil {
Expand All @@ -30,7 +30,6 @@ func (d *debounce) reset() *debounce {
f()
}
})
return d
}

func (d *debounce) cancel() {
Expand Down Expand Up @@ -61,6 +60,87 @@ func NewDebounce(duration time.Duration, f ...func()) (func(), func()) {
}, d.cancel
}

type debounceByItem struct {
mu *sync.Mutex
timer *time.Timer
count int
}

type debounceBy[T comparable] struct {
after time.Duration
mu *sync.Mutex
items map[T]*debounceByItem
callbacks []func(key T, count int)
}

func (d *debounceBy[T]) reset(key T) {
d.mu.Lock()
if _, ok := d.items[key]; !ok {
d.items[key] = &debounceByItem{
mu: new(sync.Mutex),
timer: nil,
}
}

item := d.items[key]

d.mu.Unlock()

item.mu.Lock()
defer item.mu.Unlock()

item.count++

if item.timer != nil {
item.timer.Stop()
}

item.timer = time.AfterFunc(d.after, func() {
item.mu.Lock()
count := item.count
item.count = 0
item.mu.Unlock()

for _, f := range d.callbacks {
f(key, count)
}

})
}

func (d *debounceBy[T]) cancel(key T) {
d.mu.Lock()
defer d.mu.Unlock()

if item, ok := d.items[key]; ok {
item.mu.Lock()

if item.timer != nil {
item.timer.Stop()
item.timer = nil
}

item.mu.Unlock()

delete(d.items, key)
}
}

// NewDebounceBy creates a debounced instance for each distinct key, that delays invoking functions given until after wait milliseconds have elapsed.
// Play: https://go.dev/play/p/d3Vpt6pxhY8
func NewDebounceBy[T comparable](duration time.Duration, f ...func(key T, count int)) (func(key T), func(key T)) {
d := &debounceBy[T]{
after: duration,
mu: new(sync.Mutex),
items: map[T]*debounceByItem{},
callbacks: f,
}

return func(key T) {
d.reset(key)
}, d.cancel
}

// Attempt invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a successful response is returned.
// Play: https://go.dev/play/p/3ggJZ2ZKcMj
func Attempt(maxIteration int, f func(index int) error) (int, error) {
Expand Down
56 changes: 49 additions & 7 deletions retry_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,82 @@ package lo
import (
"fmt"
"sync"
"sync/atomic"
"time"
)

func ExampleNewDebounce() {
i := 0
calls := []int{}
i := int32(0)
calls := []int32{}
mu := sync.Mutex{}

debounce, cancel := NewDebounce(time.Millisecond, func() {
mu.Lock()
defer mu.Unlock()
calls = append(calls, i)
calls = append(calls, atomic.LoadInt32(&i))
})

debounce()
i++
atomic.AddInt32(&i, 1)

time.Sleep(5 * time.Millisecond)

debounce()
i++
atomic.AddInt32(&i, 1)
debounce()
i++
atomic.AddInt32(&i, 1)
debounce()
i++
atomic.AddInt32(&i, 1)

time.Sleep(5 * time.Millisecond)

cancel()

mu.Lock()
fmt.Printf("%v", calls)
mu.Unlock()
// Output: [1 4]
}

func ExampleNewDebounceBy() {
calls := map[string][]int{}
mu := sync.Mutex{}

debounce, cancel := NewDebounceBy(time.Millisecond, func(userID string, count int) {
mu.Lock()
defer mu.Unlock()

if _, ok := calls[userID]; !ok {
calls[userID] = []int{}
}

calls[userID] = append(calls[userID], count)
})

debounce("samuel")
debounce("john")

time.Sleep(5 * time.Millisecond)

debounce("john")
debounce("john")
debounce("samuel")
debounce("john")

time.Sleep(5 * time.Millisecond)

cancel("samuel")
cancel("john")

mu.Lock()
fmt.Printf("samuel: %v\n", calls["samuel"])
fmt.Printf("john: %v\n", calls["john"])
mu.Unlock()
// Output:
// samuel: [1 1]
// john: [1 3]
}

func ExampleAttempt() {
count1, err1 := Attempt(2, func(i int) error {
if i == 0 {
Expand Down
89 changes: 89 additions & 0 deletions retry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package lo

import (
"fmt"
"sync"
"testing"
"time"

Expand Down Expand Up @@ -315,6 +316,94 @@ func TestDebounce(t *testing.T) {
}
}

func TestDebounceBy(t *testing.T) {
t.Parallel()
is := assert.New(t)

mu := sync.Mutex{}
output := map[int]int{0: 0, 1: 0, 2: 0}

f1 := func(key int, count int) {
mu.Lock()
output[key] += count
mu.Unlock()
// fmt.Printf("[key=%d] 1. Called once after 10ms when func stopped invoking!\n", key)
}
f2 := func(key int, count int) {
mu.Lock()
output[key] += count
mu.Unlock()
// fmt.Printf("[key=%d] 2. Called once after 10ms when func stopped invoking!\n", key)
}
f3 := func(key int, count int) {
mu.Lock()
output[key] += count
mu.Unlock()
// fmt.Printf("[key=%d] 3. Called once after 10ms when func stopped invoking!\n", key)
}

d1, _ := NewDebounceBy(10*time.Millisecond, f1)

// execute 3 times
for i := 0; i < 3; i++ {
for j := 0; j < 10; j++ {
for k := 0; k < 3; k++ {
d1(k)
}
}
time.Sleep(20 * time.Millisecond)
}

mu.Lock()
is.EqualValues(output[0], 30)
is.EqualValues(output[1], 30)
is.EqualValues(output[2], 30)
mu.Unlock()

d2, _ := NewDebounceBy(10*time.Millisecond, f2)

// execute once because it is always invoked and only last invoke is worked after 100ms
for i := 0; i < 3; i++ {
for j := 0; j < 5; j++ {
for k := 0; k < 3; k++ {
d2(k)
}
}
time.Sleep(5 * time.Millisecond)
}

time.Sleep(10 * time.Millisecond)

mu.Lock()
is.EqualValues(output[0], 45)
is.EqualValues(output[1], 45)
is.EqualValues(output[2], 45)
mu.Unlock()

// execute once because it is canceled after 200ms.
d3, cancel := NewDebounceBy(10*time.Millisecond, f3)
for i := 0; i < 3; i++ {
for j := 0; j < 10; j++ {
for k := 0; k < 3; k++ {
d3(k)
}
}

time.Sleep(20 * time.Millisecond)
if i == 0 {
for k := 0; k < 3; k++ {
cancel(k)
}
}
}

mu.Lock()
is.EqualValues(output[0], 75)
is.EqualValues(output[1], 75)
is.EqualValues(output[2], 75)
mu.Unlock()
}

func TestTransation(t *testing.T) {
is := assert.New(t)

Expand Down

0 comments on commit 03b3860

Please sign in to comment.