feat: Enhance real-time collaboration features with user awareness and document sharing

- Added user information (UserID, UserName, UserAvatar) to Client struct for presence tracking.
- Implemented failure handling in the broadcastMessage function to manage send failures and disconnect clients if necessary.
- Introduced document ownership and sharing capabilities:
  - Added OwnerID and Is_Public fields to Document model.
  - Created DocumentShare model for managing document sharing with permissions.
  - Implemented functions for creating, listing, and managing document shares in the Postgres store.
- Added user management functionality:
  - Created User model and associated functions for user management in the Postgres store.
  - Implemented session management with token hashing for security.
- Updated database schema with migrations for users, sessions, and document shares.
- Enhanced frontend Yjs integration with awareness event logging for user connections and disconnections.
This commit is contained in:
M1ngdaXie
2026-01-03 12:59:53 -08:00
parent 37d89b13b9
commit 7f5f32179b
21 changed files with 2064 additions and 232 deletions

View File

@@ -3,7 +3,9 @@ package hub
import (
"log"
"sync"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
@@ -14,11 +16,20 @@ type Message struct {
}
type Client struct {
ID string
Conn *websocket.Conn
send chan []byte
hub *Hub
roomID string
ID string
UserID *uuid.UUID // Authenticated user ID (nil for public share access)
UserName string // User's display name for presence
UserAvatar *string // User's avatar URL for presence
Conn *websocket.Conn
send chan []byte
sendMu sync.Mutex
sendClosed bool
hub *Hub
roomID string
mutex sync.Mutex
unregisterOnce sync.Once
failureCount int
failureMu sync.Mutex
}
type Room struct {
ID string
@@ -74,54 +85,99 @@ func (h *Hub) registerClient(client *Client) {
log.Printf("Client %s joined room %s (total clients: %d)", client.ID, client.roomID, len(room.clients))
}
func (h *Hub) unregisterClient(client *Client) {
h.mu.Lock()
defer h.mu.Unlock()
h.mu.Lock()
defer h.mu.Unlock()
room, exists := h.rooms[client.roomID]
if !exists {
log.Printf("Room %s does not exist for client %s", client.roomID, client.ID)
return
}
room.mu.Lock()
if _, ok := room.clients[client]; ok {
delete(room.clients, client)
close(client.send)
log.Printf("Client %s disconnected from room %s", client.ID, client.roomID)
}
room, exists := h.rooms[client.roomID]
if !exists {
log.Printf("Room %s does not exist for client %s", client.roomID, client.ID)
return
}
room.mu.Unlock()
log.Printf("Client %s left room %s (total clients: %d)", client.ID, client.roomID, len(room.clients))
room.mu.Lock()
defer room.mu.Unlock()
if len(room.clients) == 0 {
delete(h.rooms, client.roomID)
log.Printf("Deleted empty room with ID: %s", client.roomID)
}
if _, ok := room.clients[client]; ok {
delete(room.clients, client)
// Safely close send channel exactly once
client.sendMu.Lock()
if !client.sendClosed {
close(client.send)
client.sendClosed = true
}
client.sendMu.Unlock()
log.Printf("Client %s disconnected from room %s (total clients: %d)",
client.ID, client.roomID, len(room.clients))
}
if len(room.clients) == 0 {
delete(h.rooms, client.roomID)
log.Printf("Deleted empty room with ID: %s", client.roomID)
}
}
const (
writeWait = 10 * time.Second
pongWait = 60 * time.Second
pingPeriod = (pongWait * 9) / 10 // 54 seconds
maxSendFailures = 5
)
func (h *Hub) broadcastMessage(message *Message) {
h.mu.RLock()
room, exists := h.rooms[message.RoomID]
h.mu.RUnlock()
if !exists {
log.Printf("Room %s does not exist for broadcasting", message.RoomID)
return
}
h.mu.RLock()
room, exists := h.rooms[message.RoomID]
h.mu.RUnlock()
if !exists {
log.Printf("Room %s does not exist for broadcasting", message.RoomID)
return
}
room.mu.RLock()
defer room.mu.RUnlock()
for client := range room.clients {
if client != message.sender {
select {
case client.send <- message.Data:
default:
log.Printf("Failed to send to client %s (channel full)", client.ID)
}
}
}
room.mu.RLock()
defer room.mu.RUnlock()
for client := range room.clients {
if client != message.sender {
select {
case client.send <- message.Data:
// Success - reset failure count
client.failureMu.Lock()
client.failureCount = 0
client.failureMu.Unlock()
default:
// Failed - increment failure count
client.failureMu.Lock()
client.failureCount++
currentFailures := client.failureCount
client.failureMu.Unlock()
log.Printf("Failed to send to client %s (channel full, failures: %d/%d)",
client.ID, currentFailures, maxSendFailures)
// Disconnect if threshold exceeded
if currentFailures >= maxSendFailures {
log.Printf("Client %s exceeded max send failures, disconnecting", client.ID)
go func(c *Client) {
c.unregister()
c.Conn.Close()
}(client)
}
}
}
}
}
func (c *Client) ReadPump() {
c.Conn.SetReadDeadline(time.Now().Add(pongWait))
c.Conn.SetPongHandler(func(string) error {
c.Conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
defer func() {
c.hub.Unregister <- c
c.unregister()
c.Conn.Close()
}()
for {
@@ -141,24 +197,54 @@ func (c *Client) ReadPump() {
}
func (c *Client) WritePump() {
defer func() {
c.Conn.Close()
}()
for message := range c.send {
err := c.Conn.WriteMessage(websocket.BinaryMessage, message)
if err != nil {
log.Printf("Error writing message to client %s: %v", c.ID, err)
break
}
}
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.unregister() // NEW: Now WritePump also unregisters
c.Conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.Conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
// Hub closed the channel
c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
err := c.Conn.WriteMessage(websocket.BinaryMessage, message)
if err != nil {
log.Printf("Error writing message to client %s: %v", c.ID, err)
return
}
case <-ticker.C:
c.Conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
log.Printf("Ping failed for client %s: %v", c.ID, err)
return
}
}
}
}
func NewClient(id string, conn *websocket.Conn, hub *Hub, roomID string) *Client {
func NewClient(id string, userID *uuid.UUID, userName string, userAvatar *string, conn *websocket.Conn, hub *Hub, roomID string) *Client {
return &Client{
ID: id,
Conn: conn,
send: make(chan []byte, 256),
hub: hub,
roomID: roomID,
ID: id,
UserID: userID,
UserName: userName,
UserAvatar: userAvatar,
Conn: conn,
send: make(chan []byte, 256),
hub: hub,
roomID: roomID,
}
}
func (c *Client) unregister() {
c.unregisterOnce.Do(func() {
c.hub.Unregister <- c
})
}