Documentation Index
Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt
Use this file to discover all available pages before exploring further.
A Session represents a single WebSocket connection. Each connected client has its own session with a unique ID and associated value from authentication.
Session Lifecycle
Client connects (HTTP upgrade request)
│
▼
┌─────────────┐
│ OnAuth │ ──── Error ────▶ Connection rejected (401)
└─────────────┘
│
Returns value
│
▼
┌─────────────┐
│ Session │ ◀──── Session created with ID and value
│ Created │
└─────────────┘
│
▼
┌─────────────┐
│ Active │ ◀──── Send/Receive messages
└─────────────┘ OnMessage called for each message
│
Client disconnects
or server closes
or queue full
│
▼
┌─────────────┐
│ OnClose │ ──── Session destroyed
└─────────────┘
Session Methods
Returns the session’s unique identifier.
OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) {
sessionID := s.ID()
log.Printf("Message from session: %s", sessionID)
}
Value
Returns the value stored during authentication.
OnAuth: func(ctx context.Context, r *http.Request) (any, error) {
// Store user info
return UserInfo{ID: "123", Name: "Alice"}, nil
}
OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) {
// Retrieve user info
user := s.Value().(UserInfo)
log.Printf("Message from user: %s", user.Name)
}
Send
Queues a message for delivery to the client. Non-blocking.
// Send a message
err := s.Send(live.Message{
Topic: "notification",
Data: []byte(`{"title":"Hello"}`),
})
if err == live.ErrSessionClosed {
log.Println("Session already closed")
} else if err == live.ErrQueueFull {
log.Println("Queue full, session closed")
}
Close
Closes the connection gracefully.
IsClosed
Checks if the session is closed.
if s.IsClosed() {
return // Don't send to closed session
}
CloseError
Returns the error that caused the session to close.
OnClose: func(s *live.Session, err error) {
closeErr := s.CloseError()
if closeErr == live.ErrQueueFull {
log.Println("Client couldn't keep up")
}
}
Subscription Management
Subscriptions are managed through the server, not the session:
// Subscribe a session to a topic
server.Subscribe(s, "chat:room1")
// Unsubscribe from a topic
server.Unsubscribe(s, "chat:room1")
When a session closes, it’s automatically unsubscribed from all topics.
Common Patterns
User Authentication
type UserInfo struct {
ID string
Name string
Role string
}
server := live.New(live.Options{
OnAuth: func(ctx context.Context, r *http.Request) (any, error) {
token := r.Header.Get("Authorization")
user, err := validateToken(token)
if err != nil {
return nil, err
}
return user, nil
},
OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) {
user := s.Value().(UserInfo)
// Check permissions
if topic == "admin" && user.Role != "admin" {
s.Send(live.Message{Topic: "error", Data: []byte(`"unauthorized"`)})
return
}
// Handle message...
},
})
Auto-Subscribe on Connect
Since there’s no OnConnect hook, use OnMessage with a special “init” topic:
OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) {
user := s.Value().(UserInfo)
switch topic {
case "init":
// Subscribe to default topics
server.Subscribe(s, "user:"+user.ID)
server.Subscribe(s, "global")
// Send welcome message
s.Send(live.Message{
Topic: "welcome",
Data: []byte(`{"status":"connected"}`),
})
case "subscribe":
// Handle explicit subscription requests
var req struct{ Topic string `json:"topic"` }
json.Unmarshal(data, &req)
server.Subscribe(s, req.Topic)
}
}
Client sends init on connect:
ws.onopen = () => {
ws.send(JSON.stringify({topic: 'init', data: {}}));
};
Presence Tracking
var onlineUsers sync.Map
server := live.New(live.Options{
OnAuth: func(ctx context.Context, r *http.Request) (any, error) {
userID := r.URL.Query().Get("user")
return userID, nil
},
OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) {
userID := s.Value().(string)
if topic == "init" {
// Mark user online
onlineUsers.Store(userID, s.ID())
// Subscribe to presence
server.Subscribe(s, "presence")
// Notify others
server.Publish("presence", []byte(`{"type":"join","user":"`+userID+`"}`))
}
},
OnClose: func(s *live.Session, err error) {
userID := s.Value().(string)
// Mark offline
onlineUsers.Delete(userID)
// Notify others
server.Publish("presence", []byte(`{"type":"leave","user":"`+userID+`"}`))
},
})
Direct Messaging
// Store sessions by user ID
var userSessions sync.Map // userID -> *live.Session
OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) {
userID := s.Value().(string)
switch topic {
case "init":
userSessions.Store(userID, s)
case "dm":
var req struct {
To string `json:"to"`
Text string `json:"text"`
}
json.Unmarshal(data, &req)
// Find recipient
if recipient, ok := userSessions.Load(req.To); ok {
sess := recipient.(*live.Session)
msg, _ := json.Marshal(map[string]string{
"from": userID,
"text": req.Text,
})
sess.Send(live.Message{Topic: "dm", Data: msg})
}
}
},
OnClose: func(s *live.Session, err error) {
userID := s.Value().(string)
userSessions.Delete(userID)
},
Room Management
OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) {
userID := s.Value().(string)
switch topic {
case "join_room":
var req struct{ Room string `json:"room"` }
json.Unmarshal(data, &req)
// Subscribe to room
server.Subscribe(s, "room:"+req.Room)
// Notify room
msg, _ := json.Marshal(map[string]string{"type": "join", "user": userID})
server.Publish("room:"+req.Room, msg)
case "leave_room":
var req struct{ Room string `json:"room"` }
json.Unmarshal(data, &req)
// Notify room
msg, _ := json.Marshal(map[string]string{"type": "leave", "user": userID})
server.Publish("room:"+req.Room, msg)
// Unsubscribe
server.Unsubscribe(s, "room:"+req.Room)
case "room_message":
var req struct {
Room string `json:"room"`
Text string `json:"text"`
}
json.Unmarshal(data, &req)
msg, _ := json.Marshal(map[string]string{
"type": "message",
"user": userID,
"text": req.Text,
})
server.Publish("room:"+req.Room, msg)
}
}
Errors
| Error | Description |
|---|
ErrSessionClosed | Session is already closed |
ErrQueueFull | Send queue is full, session was closed |
Best Practices
1. Store Minimal Data in Value
// Good: just the user ID
return userID, nil
// Okay: small struct
return UserInfo{ID: "123", Role: "admin"}, nil
// Avoid: large objects
return entireUserObject, nil // Bad
2. Handle Close Gracefully
OnClose: func(s *live.Session, err error) {
userID := s.Value().(string)
// Clean up all resources
onlineUsers.Delete(userID)
userSessions.Delete(userID)
// Notify others if needed
server.Publish("presence", []byte(`{"type":"leave","user":"`+userID+`"}`))
}
3. Check Before Sending
if !s.IsClosed() {
s.Send(message)
}
4. Handle Reconnection
Clients will reconnect. Design your protocol to handle it:
// Client-side reconnection
function connect() {
ws = new WebSocket(url);
ws.onopen = () => {
// Re-initialize on every connect
ws.send(JSON.stringify({topic: 'init', data: {}}));
};
ws.onclose = () => setTimeout(connect, 1000);
}