feat(collections): add real-time updates via WebSocket (Limitation #4)
Implement real-time collection updates when books are added/removed:
Backend Changes:
- Added connManager to CollectionHandler struct
- Updated constructor to accept ConnectionManager
- Updated all NewCollectionHandler() calls in ebook.go and main.go
- Added WebSocket broadcasts in AddBooks() handler
- Added WebSocket broadcasts in BulkRemoveBooks() handler
- Broadcasts collection_updated events with:
- collection_id: Which collection changed
- action: books_added or books_removed
- book_ids: Array of affected book IDs
- count: Number of books changed
Frontend Changes:
- Added WebSocket connection in collections UI
- connectWebSocket() establishes connection to /ws/sync
- Listens for collection_updated events
- Shows toast notification on collection change
- Auto-reloads page after 1 second to show updated book list
- Auto-reconnect on disconnect (5s delay)
- Error handling for WebSocket failures
WebSocket Event Format:
{
"type": "collection_updated",
"timestamp": "2026-02-01T12:00:00Z",
"data": {
"collection_id": "uuid",
"action": "books_added",
"book_ids": ["uuid1", "uuid2"],
"count": 2
}
}
User Experience:
- When another user adds books to a collection, all connected clients see:
1. Toast notification: "Collection updated: books_added (2 books)"
2. Page auto-refreshes after 1 second
3. Updated book list displays
- Same for book removal
- Works across multiple browser tabs/devices
- No manual refresh needed
Technical Notes:
- Broadcasts to ALL connected WebSocket clients
- Client-side filtering by collection_id
- Existing progress/conflict broadcasts continue to work
- Connection manager handles broadcast distribution
Resolves Limitation #4: Real-time Collection Updates
This commit is contained in:
@@ -3,11 +3,13 @@ package handlers
|
||||
import (
|
||||
"bookmann/internal/database"
|
||||
"bookmann/internal/services"
|
||||
wsync "bookmann/internal/sync"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
@@ -17,12 +19,14 @@ import (
|
||||
type CollectionHandler struct {
|
||||
db *database.Queries
|
||||
collectionService *services.CollectionService
|
||||
connManager *wsync.ConnectionManager
|
||||
}
|
||||
|
||||
func NewCollectionHandler(db *database.Queries) *CollectionHandler {
|
||||
func NewCollectionHandler(db *database.Queries, connManager *wsync.ConnectionManager) *CollectionHandler {
|
||||
return &CollectionHandler{
|
||||
db: db,
|
||||
collectionService: services.NewCollectionService(db),
|
||||
connManager: connManager,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,12 +280,31 @@ func (h *CollectionHandler) AddBooks(c echo.Context) error {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
addedCount := 0
|
||||
var addedBookIDs []string
|
||||
for _, bookIDStr := range req.BookIDs {
|
||||
bookID, err := uuid.Parse(bookIDStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
h.collectionService.AddBookToCollection(c.Request().Context(), collectionID, bookID, userUUID)
|
||||
err = h.collectionService.AddBookToCollection(c.Request().Context(), collectionID, bookID, userUUID)
|
||||
if err == nil {
|
||||
addedCount++
|
||||
addedBookIDs = append(addedBookIDs, bookID.String())
|
||||
}
|
||||
}
|
||||
|
||||
if addedCount > 0 && h.connManager != nil {
|
||||
h.connManager.Broadcast(wsync.BroadcastMessage{
|
||||
Type: "collection_updated",
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Data: map[string]interface{}{
|
||||
"collection_id": collectionID.String(),
|
||||
"action": "books_added",
|
||||
"book_ids": addedBookIDs,
|
||||
"count": addedCount,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
@@ -325,6 +348,7 @@ func (h *CollectionHandler) BulkRemoveBooks(c echo.Context) error {
|
||||
}
|
||||
|
||||
removedCount := 0
|
||||
var removedBookIDs []string
|
||||
for _, bookIDStr := range req.BookIDs {
|
||||
bookID, err := uuid.Parse(bookIDStr)
|
||||
if err != nil {
|
||||
@@ -334,9 +358,23 @@ func (h *CollectionHandler) BulkRemoveBooks(c echo.Context) error {
|
||||
err = h.collectionService.RemoveBookFromCollection(c.Request().Context(), collectionID, bookID)
|
||||
if err == nil {
|
||||
removedCount++
|
||||
removedBookIDs = append(removedBookIDs, bookID.String())
|
||||
}
|
||||
}
|
||||
|
||||
if removedCount > 0 && h.connManager != nil {
|
||||
h.connManager.Broadcast(wsync.BroadcastMessage{
|
||||
Type: "collection_updated",
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Data: map[string]interface{}{
|
||||
"collection_id": collectionID.String(),
|
||||
"action": "books_removed",
|
||||
"book_ids": removedBookIDs,
|
||||
"count": removedCount,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"removed": removedCount,
|
||||
"total": len(req.BookIDs),
|
||||
|
||||
Reference in New Issue
Block a user