From 25f12aa49ee8ff43116c62f94f0f342347d225ed Mon Sep 17 00:00:00 2001 From: maddalax Date: Tue, 1 Oct 2024 13:42:14 -0500 Subject: [PATCH] some fixes --- examples/chat/Dockerfile | 12 +++----- examples/chat/chat/broadcast.go | 38 +++++++++++++++++------ examples/chat/chat/component.go | 10 +++--- examples/chat/pages/chat.$id.go | 8 +++-- examples/chat/ws/handler.go | 1 - examples/chat/ws/manager.go | 54 +++++++++++++++++++++++---------- 6 files changed, 82 insertions(+), 41 deletions(-) diff --git a/examples/chat/Dockerfile b/examples/chat/Dockerfile index 7bed324..a738a1b 100644 --- a/examples/chat/Dockerfile +++ b/examples/chat/Dockerfile @@ -1,9 +1,5 @@ # Stage 1: Build the Go binary -FROM golang:1.23-alpine AS builder - -RUN apk update -RUN apk add git -RUN apk add curl +FROM golang:1.23 AS builder # Set the working directory inside the container WORKDIR /app @@ -18,7 +14,9 @@ RUN go mod download COPY . . # Build the Go binary for Linux -RUN GOPRIVATE=github.com/maddalax GOPROXY=direct go run github.com/maddalax/htmgo/cli/htmgo@latest build +RUN CGO_ENABLED=0 GOPRIVATE=github.com/maddalax LOG_LEVEL=debug go run github.com/maddalax/htmgo/cli/htmgo@latest build + +RUN CGO_ENABLED=1 GOOS=linux go build -o ./dist -a -ldflags '-linkmode external -extldflags "-static"' . # Stage 2: Create the smallest possible image @@ -35,4 +33,4 @@ EXPOSE 3000 # Command to run the binary -CMD ["./starter-template"] \ No newline at end of file +CMD ["./chat"] diff --git a/examples/chat/chat/broadcast.go b/examples/chat/chat/broadcast.go index 7c9e2a7..690ce5c 100644 --- a/examples/chat/chat/broadcast.go +++ b/examples/chat/chat/broadcast.go @@ -52,36 +52,51 @@ func (m *Manager) OnConnected(e ws.SocketEvent) { return } - fmt.Printf("User %s connected to room %s\n", e.Id, e.RoomId) - user, err := m.queries.GetUserBySessionId(context.Background(), e.Id) if err != nil { + m.socketManager.CloseWithError(e.Id, websocket.StatusPolicyViolation, "invalid user") return } - m.socketManager.BroadcastText(h.Render(ConnectedUsers(user.Name))) + fmt.Printf("User %s connected to %s\n", user.Name, e.RoomId) + + // backfill all existing clients to the connected client m.socketManager.ForEachSocket(e.RoomId, func(conn ws.SocketConnection) { - if conn.Id == e.Id { - return - } user, err := m.queries.GetUserBySessionId(context.Background(), conn.Id) if err != nil { return } - m.socketManager.SendText(e.Id, h.Render(ConnectedUsers(user.Name))) + isMe := conn.Id == e.Id + fmt.Printf("Sending connected user %s to %s\n", user.Name, e.Id) + m.socketManager.SendText(e.Id, h.Render(ConnectedUsers(user.Name, isMe))) }) + // send the connected user to all existing clients + m.socketManager.BroadcastText( + e.RoomId, + h.Render(ConnectedUsers(user.Name, false)), + func(conn ws.SocketConnection) bool { + return conn.Id != e.Id + }, + ) + go m.backFill(e.Id, e.RoomId) } func (m *Manager) OnDisconnected(e ws.SocketEvent) { - fmt.Printf("User %s disconnected\n", e.Id) user, err := m.queries.GetUserBySessionId(context.Background(), e.Id) if err != nil { return } - m.socketManager.BroadcastText(h.Render(ConnectedUser(user.Name, true))) + room, err := m.service.GetRoom(e.RoomId) + if err != nil { + return + } + fmt.Printf("User %s disconnected from %s\n", user.Name, room.ID) + m.socketManager.BroadcastText(room.ID, h.Render(ConnectedUser(user.Name, true, false)), func(conn ws.SocketConnection) bool { + return conn.Id != e.Id + }) } func (m *Manager) backFill(socketId string, roomId string) { @@ -103,7 +118,6 @@ func (m *Manager) backFill(socketId string, roomId string) { } func (m *Manager) onMessage(e ws.SocketEvent) { - fmt.Printf("Received message from %s: %v\n", e.Id, e.Payload) message := e.Payload["message"].(string) if message == "" { @@ -125,7 +139,11 @@ func (m *Manager) onMessage(e ws.SocketEvent) { if saved != nil { m.socketManager.BroadcastText( + e.RoomId, h.Render(MessageRow(saved)), + func(conn ws.SocketConnection) bool { + return true + }, ) } } diff --git a/examples/chat/chat/component.go b/examples/chat/chat/component.go index 38ff752..7f88ad2 100644 --- a/examples/chat/chat/component.go +++ b/examples/chat/chat/component.go @@ -24,25 +24,27 @@ func MessageRow(message *Message) *h.Element { ) } -func ConnectedUsers(username string) *h.Element { +func ConnectedUsers(username string, isMe bool) *h.Element { return h.Ul( h.Attribute("hx-swap", "none"), h.Attribute("hx-swap-oob", "beforeend"), h.Id("connected-users"), h.Class("flex flex-col"), // This would be populated dynamically with connected users - ConnectedUser(username, false), + ConnectedUser(username, false, isMe), ) } -func ConnectedUser(username string, remove bool) *h.Element { +func ConnectedUser(username string, remove bool, isMe bool) *h.Element { id := fmt.Sprintf("connected-user-%s", strings.ReplaceAll(username, "#", "-")) if remove { return h.Div(h.Id(id), h.Attribute("hx-swap-oob", "delete")) } return h.Li( h.Id(id), - h.Class("truncate text-slate-700"), + h.ClassX("truncate text-slate-700", h.ClassMap{ + "font-bold": isMe, + }), h.Text(username), ) } diff --git a/examples/chat/pages/chat.$id.go b/examples/chat/pages/chat.$id.go index 6668c17..f38baab 100644 --- a/examples/chat/pages/chat.$id.go +++ b/examples/chat/pages/chat.$id.go @@ -28,14 +28,16 @@ func ChatRoom(ctx *h.RequestContext) *h.Page { h.HxOnWsClose( js.EvalJs(fmt.Sprintf(` const reason = e.detail.event.reason - if(['invalid room', 'no session'].includes(reason)) { + if(['invalid room', 'no session', 'invalid user'].includes(reason)) { window.location.href = '/?roomId=%s'; } else if(e.detail.event.code === 1011) { window.location.reload() + } else if (e.detail.event.code === 1008 || e.detail.event.code === 1006) { + window.location.href = '/?roomId=%s'; } else { console.error('Connection closed:', e.detail.event) } - `, roomId)), + `, roomId, roomId)), ), h.Class("flex flex-row min-h-screen bg-neutral-100"), @@ -105,7 +107,7 @@ func UserSidebar() *h.Element { h.Class("pt-[67px] min-w-48 w-48 bg-neutral-200 p-4 flex flex-col justify-between gap-3 rounded-l-lg"), h.Div( h.H3F("Connected Users", h.Class("text-lg font-bold")), - chat.ConnectedUsers(""), + chat.ConnectedUsers("", false), ), h.A( h.Class("cursor-pointer"), diff --git a/examples/chat/ws/handler.go b/examples/chat/ws/handler.go index dc0a79a..b4bc9bb 100644 --- a/examples/chat/ws/handler.go +++ b/examples/chat/ws/handler.go @@ -55,7 +55,6 @@ func Handle() http.HandlerFunc { var v map[string]any err = wsjson.Read(context.Background(), c, &v) if err != nil { - slog.Error("failed to read message", slog.String("room_id", roomId)) manager.CloseWithError(sessionId, websocket.StatusInternalError, "failed to read message") return } diff --git a/examples/chat/ws/manager.go b/examples/chat/ws/manager.go index fddea04..1c03baa 100644 --- a/examples/chat/ws/manager.go +++ b/examples/chat/ws/manager.go @@ -28,21 +28,25 @@ type SocketConnection struct { } type SocketManager struct { - sockets *xsync.MapOf[string, SocketConnection] + sockets *xsync.MapOf[string, *xsync.MapOf[string, SocketConnection]] + idToRoom *xsync.MapOf[string, string] listeners []chan SocketEvent } func NewSocketManager() *SocketManager { return &SocketManager{ - sockets: xsync.NewMapOf[string, SocketConnection](), + sockets: xsync.NewMapOf[string, *xsync.MapOf[string, SocketConnection]](), + idToRoom: xsync.NewMapOf[string, string](), } } func (manager *SocketManager) ForEachSocket(roomId string, cb func(conn SocketConnection)) { - manager.sockets.Range(func(id string, conn SocketConnection) bool { - if conn.RoomId == roomId { - cb(conn) - } + sockets, ok := manager.sockets.Load(roomId) + if !ok { + return + } + sockets.Range(func(id string, conn SocketConnection) bool { + cb(conn) return true }) } @@ -74,15 +78,23 @@ func (manager *SocketManager) OnMessage(id string, message map[string]any) { } func (manager *SocketManager) Add(roomId string, id string, conn *websocket.Conn) { - manager.sockets.Store(id, SocketConnection{ + manager.idToRoom.Store(id, roomId) + + sockets, ok := manager.sockets.LoadOrCompute(roomId, func() *xsync.MapOf[string, SocketConnection] { + return xsync.NewMapOf[string, SocketConnection]() + }) + + sockets.Store(id, SocketConnection{ Id: id, Conn: conn, RoomId: roomId, }) - s, ok := manager.sockets.Load(id) + + s, ok := sockets.Load(id) if !ok { return } + manager.dispatch(SocketEvent{ Id: s.Id, Type: ConnectedEvent, @@ -122,26 +134,36 @@ func (manager *SocketManager) Disconnect(id string) { } func (manager *SocketManager) Get(id string) *SocketConnection { - conn, ok := manager.sockets.Load(id) + roomId, ok := manager.idToRoom.Load(id) if !ok { return nil } + sockets, ok := manager.sockets.Load(roomId) + if !ok { + return nil + } + conn, ok := sockets.Load(id) return &conn } -func (manager *SocketManager) Broadcast(message []byte, messageType websocket.MessageType) { +func (manager *SocketManager) Broadcast(roomId string, message []byte, messageType websocket.MessageType, predicate func(conn SocketConnection) bool) { ctx := context.Background() - manager.sockets.Range(func(id string, conn SocketConnection) bool { - err := conn.Conn.Write(ctx, messageType, message) - if err != nil { - manager.Disconnect(id) + sockets, ok := manager.sockets.Load(roomId) + + if !ok { + return + } + + sockets.Range(func(id string, conn SocketConnection) bool { + if predicate(conn) { + conn.Conn.Write(ctx, messageType, message) } return true }) } -func (manager *SocketManager) BroadcastText(message string) { - manager.Broadcast([]byte(message), websocket.MessageText) +func (manager *SocketManager) BroadcastText(roomId string, message string, predicate func(conn SocketConnection) bool) { + manager.Broadcast(roomId, []byte(message), websocket.MessageText, predicate) } func (manager *SocketManager) SendText(id string, message string) {