feat: modernise to Go 1.26 iterators and stdlib helpers
Add AllClients, AllChannels, AllSubscriptions iter.Seq iterators. Use slices.Collect(maps.Keys()) in SendToChannel/Subscriptions, range-over-int in calculateBackoff and benchmarks. Co-Authored-By: Gemini <noreply@google.com> Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
c942931657
commit
3d6f14cd91
3 changed files with 91 additions and 12 deletions
15
go.sum
15
go.sum
|
|
@ -4,25 +4,40 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||||
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||||
|
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
37
ws.go
37
ws.go
|
|
@ -62,7 +62,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"iter"
|
||||||
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -349,10 +352,7 @@ func (h *Hub) SendToChannel(channel string, msg Message) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy client references under lock to avoid races during iteration
|
// Copy client references under lock to avoid races during iteration
|
||||||
targets := make([]*Client, 0, len(clients))
|
targets := slices.Collect(maps.Keys(clients))
|
||||||
for client := range clients {
|
|
||||||
targets = append(targets, client)
|
|
||||||
}
|
|
||||||
h.mu.RUnlock()
|
h.mu.RUnlock()
|
||||||
|
|
||||||
for _, client := range targets {
|
for _, client := range targets {
|
||||||
|
|
@ -429,6 +429,20 @@ func (h *Hub) ChannelSubscriberCount(channel string) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AllClients returns an iterator for all connected clients.
|
||||||
|
func (h *Hub) AllClients() iter.Seq[*Client] {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
return slices.Values(slices.Collect(maps.Keys(h.clients)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllChannels returns an iterator for all active channels.
|
||||||
|
func (h *Hub) AllChannels() iter.Seq[string] {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
return slices.Values(slices.Collect(maps.Keys(h.channels)))
|
||||||
|
}
|
||||||
|
|
||||||
// HubStats contains hub statistics.
|
// HubStats contains hub statistics.
|
||||||
type HubStats struct {
|
type HubStats struct {
|
||||||
Clients int `json:"clients"`
|
Clients int `json:"clients"`
|
||||||
|
|
@ -586,11 +600,14 @@ func (c *Client) Subscriptions() []string {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
result := make([]string, 0, len(c.subscriptions))
|
return slices.Collect(maps.Keys(c.subscriptions))
|
||||||
for channel := range c.subscriptions {
|
}
|
||||||
result = append(result, channel)
|
|
||||||
}
|
// AllSubscriptions returns an iterator for the client's current subscriptions.
|
||||||
return result
|
func (c *Client) AllSubscriptions() iter.Seq[string] {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return slices.Values(slices.Collect(maps.Keys(c.subscriptions)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the client connection.
|
// Close closes the client connection.
|
||||||
|
|
@ -796,7 +813,7 @@ func (rc *ReconnectingClient) setState(state ConnectionState) {
|
||||||
|
|
||||||
func (rc *ReconnectingClient) calculateBackoff(attempt int) time.Duration {
|
func (rc *ReconnectingClient) calculateBackoff(attempt int) time.Duration {
|
||||||
backoff := rc.config.InitialBackoff
|
backoff := rc.config.InitialBackoff
|
||||||
for i := 1; i < attempt; i++ {
|
for range attempt - 1 {
|
||||||
backoff = time.Duration(float64(backoff) * rc.config.BackoffMultiplier)
|
backoff = time.Duration(float64(backoff) * rc.config.BackoffMultiplier)
|
||||||
if backoff > rc.config.MaxBackoff {
|
if backoff > rc.config.MaxBackoff {
|
||||||
backoff = rc.config.MaxBackoff
|
backoff = rc.config.MaxBackoff
|
||||||
|
|
|
||||||
51
ws_test.go
51
ws_test.go
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -442,6 +443,52 @@ func TestClient_Subscriptions(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClient_AllSubscriptions(t *testing.T) {
|
||||||
|
t.Run("returns iterator over subscriptions", func(t *testing.T) {
|
||||||
|
client := &Client{subscriptions: make(map[string]bool)}
|
||||||
|
client.subscriptions["sub1"] = true
|
||||||
|
client.subscriptions["sub2"] = true
|
||||||
|
|
||||||
|
subs := slices.Collect(client.AllSubscriptions())
|
||||||
|
assert.Len(t, subs, 2)
|
||||||
|
assert.Contains(t, subs, "sub1")
|
||||||
|
assert.Contains(t, subs, "sub2")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHub_AllClients(t *testing.T) {
|
||||||
|
t.Run("returns iterator over all clients", func(t *testing.T) {
|
||||||
|
hub := NewHub()
|
||||||
|
client1 := &Client{subscriptions: make(map[string]bool)}
|
||||||
|
client2 := &Client{subscriptions: make(map[string]bool)}
|
||||||
|
|
||||||
|
hub.mu.Lock()
|
||||||
|
hub.clients[client1] = true
|
||||||
|
hub.clients[client2] = true
|
||||||
|
hub.mu.Unlock()
|
||||||
|
|
||||||
|
clients := slices.Collect(hub.AllClients())
|
||||||
|
assert.Len(t, clients, 2)
|
||||||
|
assert.Contains(t, clients, client1)
|
||||||
|
assert.Contains(t, clients, client2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHub_AllChannels(t *testing.T) {
|
||||||
|
t.Run("returns iterator over all active channels", func(t *testing.T) {
|
||||||
|
hub := NewHub()
|
||||||
|
hub.mu.Lock()
|
||||||
|
hub.channels["ch1"] = make(map[*Client]bool)
|
||||||
|
hub.channels["ch2"] = make(map[*Client]bool)
|
||||||
|
hub.mu.Unlock()
|
||||||
|
|
||||||
|
channels := slices.Collect(hub.AllChannels())
|
||||||
|
assert.Len(t, channels, 2)
|
||||||
|
assert.Contains(t, channels, "ch1")
|
||||||
|
assert.Contains(t, channels, "ch2")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestMessage_JSON(t *testing.T) {
|
func TestMessage_JSON(t *testing.T) {
|
||||||
t.Run("marshals correctly", func(t *testing.T) {
|
t.Run("marshals correctly", func(t *testing.T) {
|
||||||
msg := Message{
|
msg := Message{
|
||||||
|
|
@ -1390,7 +1437,7 @@ func BenchmarkBroadcast(b *testing.B) {
|
||||||
msg := Message{Type: TypeEvent, Data: "benchmark"}
|
msg := Message{Type: TypeEvent, Data: "benchmark"}
|
||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for range b.N {
|
||||||
_ = hub.Broadcast(msg)
|
_ = hub.Broadcast(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1416,7 +1463,7 @@ func BenchmarkSendToChannel(b *testing.B) {
|
||||||
msg := Message{Type: TypeEvent, Data: "benchmark"}
|
msg := Message{Type: TypeEvent, Data: "benchmark"}
|
||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for range b.N {
|
||||||
_ = hub.SendToChannel("bench-channel", msg)
|
_ = hub.SendToChannel("bench-channel", msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue