Skip to content

Commit 2a743bc

Browse files
MagicalTuxclaude
andcommitted
Make key type generic with Group[K comparable, T any]
Allow any comparable type as key, not just strings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent c56798a commit 2a743bc

File tree

3 files changed

+45
-24
lines changed

3 files changed

+45
-24
lines changed

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
"github.com/KarpelesLab/unison"
2121
)
2222

23-
var group unison.Group[string]
23+
var group unison.Group[string, string]
2424

2525
func main() {
2626
result, err := group.Do("my-key", func() (string, error) {
@@ -44,7 +44,7 @@ func main() {
4444
## Example: Caching Expensive Operations
4545

4646
```go
47-
var userLoader unison.Group[*User]
47+
var userLoader unison.Group[string, *User]
4848

4949
func GetUser(id string) (*User, error) {
5050
return userLoader.Do(id, func() (*User, error) {
@@ -58,7 +58,7 @@ func GetUser(id string) (*User, error) {
5858
## Example: Preventing Thundering Herd
5959

6060
```go
61-
var cacheRefresh unison.Group[[]Product]
61+
var cacheRefresh unison.Group[string, []Product]
6262

6363
func GetProducts() ([]Product, error) {
6464
return cacheRefresh.Do("products", func() ([]Product, error) {
@@ -72,7 +72,7 @@ func GetProducts() ([]Product, error) {
7272
## Example: Time-Based Caching with DoUntil
7373

7474
```go
75-
var configLoader unison.Group[*Config]
75+
var configLoader unison.Group[string, *Config]
7676

7777
func GetConfig() (*Config, error) {
7878
return configLoader.DoUntil("config", 5*time.Minute, func() (*Config, error) {
@@ -85,19 +85,20 @@ func GetConfig() (*Config, error) {
8585

8686
## API
8787

88-
### `Group[T any]`
88+
### `Group[K comparable, T any]`
8989

90-
A generic type that manages in-flight function calls. Zero-value is ready to use.
90+
A generic type that manages in-flight function calls. `K` is the key type (must be comparable), `T` is the result type. Zero-value is ready to use.
9191

9292
```go
93-
var g unison.Group[string]
93+
var g unison.Group[string, string] // string keys, string values
94+
var g unison.Group[int, *User] // int keys, *User values
9495
```
9596

96-
### `(*Group[T]) Do(key string, fn func() (T, error)) (T, error)`
97+
### `(*Group[K, T]) Do(key K, fn func() (T, error)) (T, error)`
9798

9899
Executes `fn` for the given `key`, ensuring only one execution is in-flight at a time per key. Concurrent callers with the same key wait for the first caller's result. Once complete, subsequent calls trigger a new execution.
99100

100-
### `(*Group[T]) DoUntil(key string, dur time.Duration, fn func() (T, error)) (T, error)`
101+
### `(*Group[K, T]) DoUntil(key K, dur time.Duration, fn func() (T, error)) (T, error)`
101102

102103
Like `Do`, but caches the result for the specified duration. Subsequent calls within the cache window return the cached result without executing `fn` again. After expiration, the next call triggers a new execution.
103104

unison.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,19 @@ type call[T any] struct {
1616

1717
// Group represents a class of work and forms a namespace in which
1818
// units of work can be executed with duplicate suppression.
19-
type Group[T any] struct {
19+
type Group[K comparable, T any] struct {
2020
mu sync.Mutex
21-
m map[string]*call[T]
21+
m map[K]*call[T]
2222
}
2323

2424
// Do executes and returns the results of the given function, making
2525
// sure that only one execution is in-flight for a given key at a time.
2626
// If a duplicate comes in, the duplicate caller waits for the original
2727
// to complete and receives the same results.
28-
func (g *Group[T]) Do(key string, fn func() (T, error)) (T, error) {
28+
func (g *Group[K, T]) Do(key K, fn func() (T, error)) (T, error) {
2929
g.mu.Lock()
3030
if g.m == nil {
31-
g.m = make(map[string]*call[T])
31+
g.m = make(map[K]*call[T])
3232
}
3333

3434
if c, ok := g.m[key]; ok {
@@ -57,10 +57,10 @@ func (g *Group[T]) Do(key string, fn func() (T, error)) (T, error) {
5757
// DoUntil is like Do but caches the result for the specified duration.
5858
// Subsequent calls within the cache window return the cached result without
5959
// executing the function again.
60-
func (g *Group[T]) DoUntil(key string, dur time.Duration, fn func() (T, error)) (T, error) {
60+
func (g *Group[K, T]) DoUntil(key K, dur time.Duration, fn func() (T, error)) (T, error) {
6161
g.mu.Lock()
6262
if g.m == nil {
63-
g.m = make(map[string]*call[T])
63+
g.m = make(map[K]*call[T])
6464
}
6565

6666
if c, ok := g.m[key]; ok {

unison_test.go

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
)
1010

1111
func TestDo(t *testing.T) {
12-
var g Group[string]
12+
var g Group[string, string]
1313
v, err := g.Do("key", func() (string, error) {
1414
return "bar", nil
1515
})
@@ -22,7 +22,7 @@ func TestDo(t *testing.T) {
2222
}
2323

2424
func TestDoErr(t *testing.T) {
25-
var g Group[string]
25+
var g Group[string, string]
2626
someErr := errors.New("some error")
2727
v, err := g.Do("key", func() (string, error) {
2828
return "", someErr
@@ -36,7 +36,7 @@ func TestDoErr(t *testing.T) {
3636
}
3737

3838
func TestDoDupSuppress(t *testing.T) {
39-
var g Group[string]
39+
var g Group[string, string]
4040
var calls atomic.Int32
4141
var wg sync.WaitGroup
4242

@@ -68,7 +68,7 @@ func TestDoDupSuppress(t *testing.T) {
6868
}
6969

7070
func TestDoNewCallAfterComplete(t *testing.T) {
71-
var g Group[int]
71+
var g Group[string, int]
7272
var calls atomic.Int32
7373

7474
fn := func() (int, error) {
@@ -90,7 +90,7 @@ func TestDoNewCallAfterComplete(t *testing.T) {
9090
}
9191

9292
func TestDoDifferentKeys(t *testing.T) {
93-
var g Group[string]
93+
var g Group[string, string]
9494
var calls atomic.Int32
9595
var wg sync.WaitGroup
9696

@@ -125,7 +125,7 @@ func TestDoDifferentKeys(t *testing.T) {
125125
}
126126

127127
func TestDoUntil(t *testing.T) {
128-
var g Group[string]
128+
var g Group[string, string]
129129
v, err := g.DoUntil("key", time.Second, func() (string, error) {
130130
return "bar", nil
131131
})
@@ -138,7 +138,7 @@ func TestDoUntil(t *testing.T) {
138138
}
139139

140140
func TestDoUntilCached(t *testing.T) {
141-
var g Group[int]
141+
var g Group[string, int]
142142
var calls atomic.Int32
143143

144144
fn := func() (int, error) {
@@ -158,7 +158,7 @@ func TestDoUntilCached(t *testing.T) {
158158
}
159159

160160
func TestDoUntilExpired(t *testing.T) {
161-
var g Group[int]
161+
var g Group[string, int]
162162
var calls atomic.Int32
163163

164164
fn := func() (int, error) {
@@ -181,7 +181,7 @@ func TestDoUntilExpired(t *testing.T) {
181181
}
182182

183183
func TestDoUntilDupSuppress(t *testing.T) {
184-
var g Group[string]
184+
var g Group[string, string]
185185
var calls atomic.Int32
186186
var wg sync.WaitGroup
187187

@@ -211,3 +211,23 @@ func TestDoUntilDupSuppress(t *testing.T) {
211211
t.Errorf("number of calls = %d; want 1", got)
212212
}
213213
}
214+
215+
func TestDoIntKey(t *testing.T) {
216+
var g Group[int, string]
217+
var calls atomic.Int32
218+
219+
fn := func() (string, error) {
220+
calls.Add(1)
221+
return "value", nil
222+
}
223+
224+
v1, _ := g.Do(123, fn)
225+
v2, _ := g.Do(456, fn)
226+
227+
if v1 != "value" || v2 != "value" {
228+
t.Errorf("values = %v, %v; want value, value", v1, v2)
229+
}
230+
if got := calls.Load(); got != 2 {
231+
t.Errorf("number of calls = %d; want 2", got)
232+
}
233+
}

0 commit comments

Comments
 (0)