Skip to content

Commit 1f2d78e

Browse files
authored
Merge pull request #232 from mojtabaRKS/feat-one-to-one-conn-hub
Feat one to one conn hub
2 parents 7ebba2c + 5daaf1c commit 1f2d78e

File tree

11 files changed

+497
-26
lines changed

11 files changed

+497
-26
lines changed

PER_CONNECTION_HUB_EXAMPLE.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Per-Connection Hub Factory Example
2+
3+
This example demonstrates how to use the new `PerConnectionHubFactory` option to create a 1:1 mapping between client connections and hub instances.
4+
5+
## Problem
6+
7+
Previously, SignalR servers had two options for hub management:
8+
9+
1. **UseHub**: One hub instance shared across all connections (shared state)
10+
2. **HubFactory/SimpleHubFactory**: New hub instance created for every method call (no persistent state)
11+
12+
Neither option provided a way to have one hub instance per connection that persists for the lifetime of that connection.
13+
14+
## Solution
15+
16+
The new `PerConnectionHubFactory` option creates one hub instance per connection and reuses it for all method invocations on that connection. This provides:
17+
18+
- **1:1 mapping**: One hub instance per client connection
19+
- **Persistent state**: Hub instance maintains state throughout the connection lifetime
20+
- **Connection isolation**: Each connection has its own isolated hub instance
21+
- **Memory efficiency**: Hub instances are cleaned up when connections close
22+
23+
## Usage
24+
25+
```go
26+
package main
27+
28+
import (
29+
"context"
30+
"github.com/philippseith/signalr"
31+
)
32+
33+
// Define your hub with connection-specific state
34+
type ChatHub struct {
35+
signalr.Hub
36+
connectionID string
37+
username string
38+
messageCount int
39+
}
40+
41+
func (h *ChatHub) OnConnected(connectionID string) {
42+
h.connectionID = connectionID
43+
h.messageCount = 0
44+
// Initialize connection-specific state
45+
}
46+
47+
func (h *ChatHub) SendMessage(message string) {
48+
h.messageCount++
49+
// Send message to all clients
50+
h.Clients().All().Send("ReceiveMessage", h.username, message)
51+
}
52+
53+
func (h *ChatHub) SetUsername(username string) {
54+
h.username = username
55+
}
56+
57+
func (h *ChatHub) GetStats() map[string]interface{} {
58+
return map[string]interface{}{
59+
"connectionID": h.connectionID,
60+
"username": h.username,
61+
"messageCount": h.messageCount,
62+
}
63+
}
64+
65+
func main() {
66+
ctx := context.Background()
67+
68+
// Create server with PerConnectionHubFactory
69+
server, err := signalr.NewServer(ctx,
70+
signalr.PerConnectionHubFactory(func(connectionID string) signalr.HubInterface {
71+
return &ChatHub{}
72+
}),
73+
signalr.Logger(signalr.StructuredLogger(nil), false),
74+
)
75+
if err != nil {
76+
panic(err)
77+
}
78+
79+
// Use the server as usual
80+
// Each connection will get its own ChatHub instance
81+
// that persists for the lifetime of the connection
82+
}
83+
```
84+
85+
## How It Works
86+
87+
1. **Connection Establishment**: When a client connects, `PerConnectionHubFactory` is called with the connection ID
88+
2. **Hub Creation**: A new hub instance is created and stored in the server's connection map
89+
3. **Method Invocation**: All subsequent method calls on that connection use the same hub instance
90+
4. **Connection Cleanup**: When the connection closes, the hub instance is automatically removed from the map
91+
92+
## Benefits
93+
94+
- **Stateful Hubs**: Hubs can maintain connection-specific state (user sessions, counters, etc.)
95+
- **Connection Isolation**: Each client gets its own isolated hub instance
96+
- **Memory Management**: Automatic cleanup prevents memory leaks
97+
- **Performance**: No need to recreate hub instances for each method call
98+
- **Flexibility**: Factory function can create hubs with connection-specific configuration
99+
100+
## Comparison
101+
102+
| Option | Hub Instances | State Persistence | Use Case |
103+
|--------|---------------|-------------------|----------|
104+
| `UseHub` | 1 (shared) | Global state | Broadcast to all clients |
105+
| `HubFactory` | New per call | No state | Stateless operations |
106+
| `PerConnectionHubFactory` | 1 per connection | Per-connection state | User sessions, personal data |
107+
108+
## Migration
109+
110+
To migrate from existing patterns:
111+
112+
- **From `UseHub`**: If you need per-connection state, replace with `PerConnectionHubFactory`
113+
- **From `HubFactory`**: If you want to maintain state between method calls, use `PerConnectionHubFactory`
114+
- **New implementations**: Start with `PerConnectionHubFactory` for most use cases requiring state
115+
116+
## Example Use Cases
117+
118+
- **Chat applications**: Track user sessions, message counts, typing indicators
119+
- **Gaming**: Maintain player state, game progress, scores
120+
- **Dashboard**: Store user preferences, cached data, real-time updates
121+
- **Collaboration tools**: Track document editing state, user presence
122+
- **IoT applications**: Maintain device state, sensor data history

chatsample/main.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ func (c *chat) DoSomethingBugy() {
4444
c.Close("this is a custom error!", true)
4545
}
4646

47-
4847
func (c *chat) Panic() {
4948
panic("Don't panic!")
5049
}

chatsample/main_per_connection.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
"os"
9+
"time"
10+
11+
kitlog "github.com/go-kit/log"
12+
"github.com/philippseith/signalr"
13+
"github.com/philippseith/signalr/chatsample/middleware"
14+
"github.com/philippseith/signalr/chatsample/public"
15+
)
16+
17+
// chatPerConnection is a hub that maintains state per connection
18+
type chatPerConnection struct {
19+
signalr.Hub
20+
connectionID string
21+
username string
22+
messageCount int
23+
joinTime time.Time
24+
}
25+
26+
func (c *chatPerConnection) OnConnected(connectionID string) {
27+
c.connectionID = connectionID
28+
c.messageCount = 0
29+
c.joinTime = time.Now()
30+
c.username = fmt.Sprintf("User_%s", connectionID[:8]) // Generate username from connection ID
31+
32+
fmt.Printf("New connection: %s, Username: %s\n", connectionID, c.username)
33+
34+
// Send welcome message to the new user
35+
c.Clients().Caller().Send("ReceiveMessage", "System", fmt.Sprintf("Welcome %s! You are connection #%s", c.username, connectionID[:8]))
36+
37+
// Broadcast to all clients that a new user joined
38+
c.Clients().All().Send("ReceiveMessage", "System", fmt.Sprintf("%s has joined the chat", c.username))
39+
}
40+
41+
func (c *chatPerConnection) OnDisconnected(connectionID string) {
42+
fmt.Printf("Connection closed: %s, Username: %s, Messages sent: %d, Duration: %v\n",
43+
connectionID, c.username, c.messageCount, time.Since(c.joinTime))
44+
45+
// Broadcast to all clients that a user left
46+
c.Clients().All().Send("ReceiveMessage", "System", fmt.Sprintf("%s has left the chat", c.username))
47+
}
48+
49+
func (c *chatPerConnection) SendMessage(message string) {
50+
c.messageCount++
51+
52+
// Log the message with connection-specific info
53+
fmt.Printf("[%s] %s: %s (Message #%d)\n", c.connectionID[:8], c.username, message, c.messageCount)
54+
55+
// Send message to all clients
56+
c.Clients().All().Send("ReceiveMessage", c.username, message)
57+
}
58+
59+
func (c *chatPerConnection) SetUsername(username string) {
60+
oldUsername := c.username
61+
c.username = username
62+
63+
fmt.Printf("Username changed: %s -> %s (Connection: %s)\n", oldUsername, username, c.connectionID[:8])
64+
65+
// Broadcast username change
66+
c.Clients().All().Send("ReceiveMessage", "System", fmt.Sprintf("%s is now known as %s", oldUsername, username))
67+
}
68+
69+
func (c *chatPerConnection) GetStats() map[string]interface{} {
70+
return map[string]interface{}{
71+
"connectionID": c.connectionID[:8],
72+
"username": c.username,
73+
"messageCount": c.messageCount,
74+
"joinTime": c.joinTime.Format(time.RFC3339),
75+
"duration": time.Since(c.joinTime).String(),
76+
}
77+
}
78+
79+
func (c *chatPerConnection) Echo(message string) {
80+
c.Clients().Caller().Send("ReceiveMessage", "Echo", message)
81+
}
82+
83+
func (c *chatPerConnection) Abort() {
84+
fmt.Printf("Aborting connection: %s, Username: %s\n", c.connectionID[:8], c.username)
85+
c.Hub.Abort()
86+
}
87+
88+
func runHTTPServerPerConnection(address string) {
89+
// Create server with PerConnectionHubFactory
90+
server, _ := signalr.NewServer(context.TODO(),
91+
signalr.PerConnectionHubFactory(func(connectionID string) signalr.HubInterface {
92+
return &chatPerConnection{}
93+
}),
94+
signalr.Logger(kitlog.NewLogfmtLogger(os.Stdout), false),
95+
signalr.KeepAliveInterval(2*time.Second))
96+
97+
router := http.NewServeMux()
98+
server.MapHTTP(signalr.WithHTTPServeMux(router), "/chat")
99+
100+
fmt.Printf("Serving public content from the embedded filesystem\n")
101+
router.Handle("/", http.FileServer(http.FS(public.FS)))
102+
fmt.Printf("Listening for websocket connections on http://%s\n", address)
103+
fmt.Printf("Using PerConnectionHubFactory - each connection gets its own hub instance!\n")
104+
105+
if err := http.ListenAndServe(address, middleware.LogRequests(router)); err != nil {
106+
log.Fatal("ListenAndServe:", err)
107+
}
108+
}
109+
110+
func runHTTPClientPerConnection(address string, receiver interface{}) error {
111+
c, err := signalr.NewClient(context.Background(), nil,
112+
signalr.WithReceiver(receiver),
113+
signalr.WithConnector(func() (signalr.Connection, error) {
114+
creationCtx, _ := context.WithTimeout(context.Background(), 2*time.Second)
115+
return signalr.NewHTTPConnection(creationCtx, address)
116+
}),
117+
signalr.Logger(kitlog.NewLogfmtLogger(os.Stdout), false))
118+
if err != nil {
119+
return err
120+
}
121+
c.Start()
122+
fmt.Println("Client started")
123+
return nil
124+
}
125+
126+
type receiverPerConnection struct {
127+
signalr.Receiver
128+
}
129+
130+
func (r *receiverPerConnection) Receive(msg string) {
131+
fmt.Println(msg)
132+
// The silly client urges the server to end his connection after 10 seconds
133+
r.Server().Send("abort")
134+
}
135+
136+
func mainPerConnection() {
137+
fmt.Println("=== Per-Connection Hub Factory Demo ===")
138+
fmt.Println("This demo shows how each connection gets its own hub instance")
139+
fmt.Println("with persistent state throughout the connection lifetime.")
140+
fmt.Println()
141+
142+
go runHTTPServerPerConnection("localhost:8087")
143+
<-time.After(time.Millisecond * 2)
144+
145+
go func() {
146+
fmt.Println(runHTTPClientPerConnection("http://localhost:8087/chat", &receiverPerConnection{}))
147+
}()
148+
149+
ch := make(chan struct{})
150+
<-ch
151+
}

ext/go.mod

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ module github.com/philippseith/signalr/ext
22

33
go 1.24.2
44

5-
replace github.com/philippseith/signalr => /Users/philipp/wspc/go/moj_signalr
6-
75
require (
86
github.com/go-kit/log v0.2.1
97
github.com/google/uuid v1.6.0
@@ -34,4 +32,4 @@ require (
3432
golang.org/x/text v0.23.0 // indirect
3533
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
3634
nhooyr.io/websocket v1.8.10 // indirect
37-
)
35+
)

ext/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4
4444
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
4545
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
4646
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
47+
github.com/philippseith/signalr v0.6.3/go.mod h1:+XadWW+RWSLwWfCxyxxvnmy+00DabepYR7mOH/lkUfc=
48+
github.com/philippseith/signalr v1.1.0/go.mod h1:wTFgwM2yd1a5a60fzyBMYUK7hwKpnowUugzg7iEdo1g=
4749
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4850
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
4951
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=

0 commit comments

Comments
 (0)