Skip to content

Commit ef6d152

Browse files
authored
Publisher and generic SyncMap (#44)
* Publisher and generic SyncMap * lint * update linter version
1 parent 053e8e7 commit ef6d152

File tree

16 files changed

+937
-43
lines changed

16 files changed

+937
-43
lines changed

.github/workflows/lint.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ jobs:
1212
with:
1313
go-version-file: go.mod
1414
- name: golangci-lint
15-
uses: golangci/golangci-lint-action@v3
15+
uses: golangci/golangci-lint-action@v7
1616
with:
17-
version: v1.55.2
17+
version: v2.0.2
1818
- name: go mod tidy check
1919
uses: katexochen/go-tidy-check@v2

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ This Source Code Form is subject to the terms of the Apache Public License, vers
2020
- Resize queue length
2121
- Dequeue work in queue
2222
- View work in queue and it's current state, priority and position in queue.
23+
- Publication package provides a generic publish-subscribe (pub/sub) mechanism for Go applications. It allows you to create publications to which multiple subscribers can listen. When a message is published, it's distributed to all relevant subscribers.
24+
- **Generic:** Supports publishing and subscribing to messages of any type.
25+
- **Filtering:** Subscribers can define filters to receive only messages that meet specific criteria.
26+
- **Buffered Channels:** Subscribers receive messages through buffered channels, allowing for asynchronous message handling.
27+
- **Concurrency-Safe:** Designed for concurrent use, ensuring safe message delivery in multi-threaded environments.
28+
- **Timeout Support:** Control the maximum time spent attempting to deliver a message to a subscriber.
29+
- **Clean Shutdown:** Gracefully close publications and subscriber channels.
30+
- **Unsubscribe:** Subscribers can unsubscribe from a publication.
2331
- ValidationError
2432
- Facilitates error reflecting validation issues to a user.
2533
- Supports warnings and errors
@@ -39,6 +47,8 @@ This Source Code Form is subject to the terms of the Apache Public License, vers
3947
- A starting point for setting up and managing HTTP and/or gRPC services.
4048
- Serve both HTTP and gRPC services from single server.
4149
- Ability to configuration via configuration builder with chaining.
50+
- Generic package contains standard library types that are not yet type safe using generics and wraps them in types that are type safe using generics.
51+
- SyncMap wraps a sync.Map with a generic.
4252
- Additional tools to come!
4353

4454
## Contribution

generic/syncmap.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright (c) 2025 by Randy Bell. All rights reserved.
3+
*
4+
* This Source Code Form is subject to the terms of the Apache Public License, version 2.0. If a copy of the APL was not distributed with this file, you can obtain one at https://www.apache.org/licenses/LICENSE-2.0.txt.
5+
*/
6+
7+
package generic
8+
9+
import (
10+
"iter"
11+
"sync"
12+
)
13+
14+
// SyncMap is a generic wrapper around sync.Map that provides type safety
15+
type SyncMap[K comparable, V any] struct {
16+
*sync.Map
17+
}
18+
19+
func NewSyncMap[K comparable, V any]() *SyncMap[K, V] {
20+
return &SyncMap[K, V]{
21+
Map: &sync.Map{},
22+
}
23+
}
24+
25+
// Load returns the value stored in the map for a key, or the zero value if no
26+
// value is present. The ok result indicates whether value was found in the map.
27+
func (m *SyncMap[K, V]) Load(key K) (value V, ok bool) {
28+
v, ok := m.Map.Load(key)
29+
if !ok {
30+
return
31+
}
32+
33+
return v.(V), ok
34+
}
35+
36+
// Store sets the value for a key.
37+
func (m *SyncMap[K, V]) Store(key K, value V) {
38+
m.Map.Store(key, value)
39+
}
40+
41+
// Swap swaps the value for a key and returns the previous value if any. The loaded result reports whether the key was present.
42+
func (m *SyncMap[K, V]) Swap(key K, value V) (previous V, loaded bool) {
43+
v, l := m.Map.Swap(key, value)
44+
if !l {
45+
return
46+
}
47+
return v.(V), l
48+
}
49+
50+
// Delete deletes the value for a key.
51+
func (m *SyncMap[K, V]) Delete(key K) {
52+
m.Map.Delete(key)
53+
}
54+
55+
// LoadOrStore returns the existing value for the key if present.
56+
// Otherwise, it stores and returns the given value. The loaded result is true if the value was loaded, false if stored.
57+
func (m *SyncMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
58+
v, l := m.Map.LoadOrStore(key, value)
59+
return v.(V), l
60+
}
61+
62+
// LoadAndDelete deletes the value for a key, returning the previous value if any.
63+
// The loaded result reports whether the key was present.
64+
func (m *SyncMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
65+
v, l := m.Map.LoadAndDelete(key)
66+
if !l {
67+
return
68+
}
69+
return v.(V), l
70+
}
71+
72+
// CompareAndDelete deletes the entry for key if its value is equal to old.
73+
// The old value must be of a comparable type.
74+
func (m *SyncMap[K, V]) CompareAndDelete(key K, old V) bool {
75+
return m.Map.CompareAndDelete(key, old)
76+
}
77+
78+
// CompareAndSwap swaps the old and new values for key if the value stored in the map is equal to old.
79+
func (m *SyncMap[K, V]) CompareAndSwap(key K, old V, new V) bool {
80+
return m.Map.CompareAndSwap(key, old, new)
81+
}
82+
83+
// Range calls f sequentially for each key and value present in the map. If f returns false, range stops the iteration.
84+
func (m *SyncMap[K, V]) Range(f func(key K, value V) bool) {
85+
m.Map.Range(func(k any, v any) bool {
86+
return f(k.(K), v.(V))
87+
})
88+
}
89+
90+
// Iterate returns an iterator that can be used to iterate over the map.
91+
func (m *SyncMap[K, V]) Iterate() iter.Seq2[K, V] {
92+
return func(yield func(K, V) bool) {
93+
for anyKey, anyValue := range m.Map.Range {
94+
if !yield(
95+
anyKey.(K),
96+
anyValue.(V),
97+
) {
98+
break
99+
}
100+
}
101+
}
102+
}

generic/syncmap_test.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/*
2+
* Copyright (c) 2025 by Randy Bell. All rights reserved.
3+
*
4+
* This Source Code Form is subject to the terms of the Apache Public License, version 2.0. If a copy of the APL was not distributed with this file, you can obtain one at https://www.apache.org/licenses/LICENSE-2.0.txt.
5+
*/
6+
7+
package generic
8+
9+
import (
10+
"fmt"
11+
"sync"
12+
"testing"
13+
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
func TestSyncMap_Load(t *testing.T) {
18+
m := SyncMap[string, int]{Map: &sync.Map{}}
19+
m.Store("key1", 1)
20+
value, ok := m.Load("key1")
21+
assert.True(t, ok)
22+
assert.Equal(t, 1, value)
23+
24+
value, ok = m.Load("key2")
25+
assert.False(t, ok)
26+
assert.Equal(t, 0, value)
27+
}
28+
29+
func TestSyncMap_Store(t *testing.T) {
30+
m := SyncMap[string, int]{Map: &sync.Map{}}
31+
m.Store("key1", 1)
32+
value, ok := m.Load("key1")
33+
assert.True(t, ok)
34+
assert.Equal(t, 1, value)
35+
36+
m.Store("key1", 2)
37+
value, ok = m.Load("key1")
38+
assert.True(t, ok)
39+
assert.Equal(t, 2, value)
40+
}
41+
42+
func TestSyncMap_Swap(t *testing.T) {
43+
m := SyncMap[string, int]{Map: &sync.Map{}}
44+
m.Store("key1", 1)
45+
previous, loaded := m.Swap("key1", 2)
46+
assert.True(t, loaded)
47+
assert.Equal(t, 1, previous)
48+
49+
value, ok := m.Load("key1")
50+
assert.True(t, ok)
51+
assert.Equal(t, 2, value)
52+
53+
previous, loaded = m.Swap("key2", 3)
54+
assert.False(t, loaded)
55+
assert.Equal(t, 0, previous)
56+
57+
value, ok = m.Load("key2")
58+
assert.True(t, ok)
59+
assert.Equal(t, 3, value)
60+
}
61+
62+
func TestSyncMap_Delete(t *testing.T) {
63+
m := SyncMap[string, int]{Map: &sync.Map{}}
64+
m.Store("key1", 1)
65+
m.Delete("key1")
66+
_, ok := m.Load("key1")
67+
assert.False(t, ok)
68+
}
69+
70+
func TestSyncMap_LoadOrStore(t *testing.T) {
71+
m := SyncMap[string, int]{Map: &sync.Map{}}
72+
actual, loaded := m.LoadOrStore("key1", 1)
73+
assert.False(t, loaded)
74+
assert.Equal(t, 1, actual)
75+
76+
actual, loaded = m.LoadOrStore("key1", 2)
77+
assert.True(t, loaded)
78+
assert.Equal(t, 1, actual)
79+
}
80+
81+
func TestSyncMap_LoadAndDelete(t *testing.T) {
82+
m := SyncMap[string, int]{Map: &sync.Map{}}
83+
m.Store("key1", 1)
84+
value, loaded := m.LoadAndDelete("key1")
85+
assert.True(t, loaded)
86+
assert.Equal(t, 1, value)
87+
88+
_, ok := m.Load("key1")
89+
assert.False(t, ok)
90+
91+
value, loaded = m.LoadAndDelete("key2")
92+
assert.False(t, loaded)
93+
assert.Equal(t, 0, value)
94+
}
95+
96+
func TestSyncMap_CompareAndDelete(t *testing.T) {
97+
m := SyncMap[string, int]{Map: &sync.Map{}}
98+
m.Store("key1", 1)
99+
deleted := m.CompareAndDelete("key1", 1)
100+
assert.True(t, deleted)
101+
_, ok := m.Load("key1")
102+
assert.False(t, ok)
103+
104+
m.Store("key2", 2)
105+
deleted = m.CompareAndDelete("key2", 3)
106+
assert.False(t, deleted)
107+
value, ok := m.Load("key2")
108+
assert.True(t, ok)
109+
assert.Equal(t, 2, value)
110+
}
111+
112+
func TestSyncMap_CompareAndSwap(t *testing.T) {
113+
m := SyncMap[string, int]{Map: &sync.Map{}}
114+
m.Store("key1", 1)
115+
swapped := m.CompareAndSwap("key1", 1, 2)
116+
assert.True(t, swapped)
117+
value, ok := m.Load("key1")
118+
assert.True(t, ok)
119+
assert.Equal(t, 2, value)
120+
121+
swapped = m.CompareAndSwap("key1", 2, 2)
122+
assert.True(t, swapped)
123+
value, ok = m.Load("key1")
124+
assert.True(t, ok)
125+
assert.Equal(t, 2, value)
126+
}
127+
128+
func TestSyncMap_Range(t *testing.T) {
129+
m := SyncMap[string, int]{Map: &sync.Map{}}
130+
m.Store("key1", 1)
131+
m.Store("key2", 2)
132+
m.Store("key3", 3)
133+
134+
count := 0
135+
m.Range(func(key string, value int) bool {
136+
count++
137+
assert.Contains(t, []string{"key1", "key2", "key3"}, key)
138+
assert.Contains(t, []int{1, 2, 3}, value)
139+
return true
140+
})
141+
assert.Equal(t, 3, count)
142+
143+
count = 0
144+
m.Range(func(key string, value int) bool {
145+
count++
146+
return key != "key2"
147+
})
148+
assert.Equal(t, 2, count)
149+
}
150+
151+
func TestSyncMap_Iterate(t *testing.T) {
152+
m := SyncMap[string, int]{Map: &sync.Map{}}
153+
m.Store("key1", 1)
154+
m.Store("key2", 2)
155+
m.Store("key3", 3)
156+
157+
count := 0
158+
it := m.Iterate()
159+
it(func(key string, value int) bool {
160+
count++
161+
assert.Contains(t, []string{"key1", "key2", "key3"}, key)
162+
assert.Contains(t, []int{1, 2, 3}, value)
163+
return true
164+
})
165+
assert.Equal(t, 3, count)
166+
167+
brokeEarly := false
168+
fmt.Println("test")
169+
it = m.Iterate()
170+
it(func(key string, value int) bool {
171+
count++
172+
if key == "key2" {
173+
brokeEarly = true
174+
return false
175+
}
176+
return true
177+
})
178+
assert.True(t, brokeEarly)
179+
}

go.mod

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
module github.com/rbell/toolchest
22

3-
go 1.22.1
4-
5-
toolchain go1.22.2
3+
go 1.23.7
64

75
require (
8-
github.com/google/btree v1.1.2
6+
github.com/google/btree v1.1.3
97
github.com/google/uuid v1.6.0
10-
github.com/richardwilkes/toolbox v1.112.0
11-
github.com/stretchr/testify v1.9.0
12-
google.golang.org/grpc v1.63.2
13-
google.golang.org/protobuf v1.33.1-0.20240408130810-98873a205002
8+
github.com/richardwilkes/toolbox v1.122.1
9+
github.com/stretchr/testify v1.10.0
10+
google.golang.org/grpc v1.71.0
11+
google.golang.org/protobuf v1.36.5
1412
)
1513

1614
require (
1715
github.com/davecgh/go-spew v1.1.1 // indirect
16+
github.com/google/go-cmp v0.7.0 // indirect
1817
github.com/kr/pretty v0.3.1 // indirect
1918
github.com/pmezard/go-difflib v1.0.0 // indirect
19+
github.com/rogpeppe/go-internal v1.14.1 // indirect
2020
github.com/stretchr/objx v0.5.2 // indirect
21-
golang.org/x/net v0.24.0 // indirect
22-
golang.org/x/sys v0.19.0 // indirect
23-
golang.org/x/text v0.14.0 // indirect
24-
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
21+
golang.org/x/net v0.37.0 // indirect
22+
golang.org/x/sys v0.31.0 // indirect
23+
golang.org/x/text v0.23.0 // indirect
24+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
2525
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
2626
gopkg.in/yaml.v3 v3.0.1 // indirect
2727
)

0 commit comments

Comments
 (0)