some fixes
This commit is contained in:
parent
f4f64fefcc
commit
25f12aa49e
6 changed files with 82 additions and 41 deletions
|
|
@ -1,9 +1,5 @@
|
||||||
# Stage 1: Build the Go binary
|
# Stage 1: Build the Go binary
|
||||||
FROM golang:1.23-alpine AS builder
|
FROM golang:1.23 AS builder
|
||||||
|
|
||||||
RUN apk update
|
|
||||||
RUN apk add git
|
|
||||||
RUN apk add curl
|
|
||||||
|
|
||||||
# Set the working directory inside the container
|
# Set the working directory inside the container
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
@ -18,7 +14,9 @@ RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build the Go binary for Linux
|
# 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
|
# Stage 2: Create the smallest possible image
|
||||||
|
|
@ -35,4 +33,4 @@ EXPOSE 3000
|
||||||
|
|
||||||
|
|
||||||
# Command to run the binary
|
# Command to run the binary
|
||||||
CMD ["./starter-template"]
|
CMD ["./chat"]
|
||||||
|
|
|
||||||
|
|
@ -52,36 +52,51 @@ func (m *Manager) OnConnected(e ws.SocketEvent) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("User %s connected to room %s\n", e.Id, e.RoomId)
|
|
||||||
|
|
||||||
user, err := m.queries.GetUserBySessionId(context.Background(), e.Id)
|
user, err := m.queries.GetUserBySessionId(context.Background(), e.Id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
m.socketManager.CloseWithError(e.Id, websocket.StatusPolicyViolation, "invalid user")
|
||||||
return
|
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) {
|
m.socketManager.ForEachSocket(e.RoomId, func(conn ws.SocketConnection) {
|
||||||
if conn.Id == e.Id {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user, err := m.queries.GetUserBySessionId(context.Background(), conn.Id)
|
user, err := m.queries.GetUserBySessionId(context.Background(), conn.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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)
|
go m.backFill(e.Id, e.RoomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) OnDisconnected(e ws.SocketEvent) {
|
func (m *Manager) OnDisconnected(e ws.SocketEvent) {
|
||||||
fmt.Printf("User %s disconnected\n", e.Id)
|
|
||||||
user, err := m.queries.GetUserBySessionId(context.Background(), e.Id)
|
user, err := m.queries.GetUserBySessionId(context.Background(), e.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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) {
|
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) {
|
func (m *Manager) onMessage(e ws.SocketEvent) {
|
||||||
fmt.Printf("Received message from %s: %v\n", e.Id, e.Payload)
|
|
||||||
message := e.Payload["message"].(string)
|
message := e.Payload["message"].(string)
|
||||||
|
|
||||||
if message == "" {
|
if message == "" {
|
||||||
|
|
@ -125,7 +139,11 @@ func (m *Manager) onMessage(e ws.SocketEvent) {
|
||||||
|
|
||||||
if saved != nil {
|
if saved != nil {
|
||||||
m.socketManager.BroadcastText(
|
m.socketManager.BroadcastText(
|
||||||
|
e.RoomId,
|
||||||
h.Render(MessageRow(saved)),
|
h.Render(MessageRow(saved)),
|
||||||
|
func(conn ws.SocketConnection) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
return h.Ul(
|
||||||
h.Attribute("hx-swap", "none"),
|
h.Attribute("hx-swap", "none"),
|
||||||
h.Attribute("hx-swap-oob", "beforeend"),
|
h.Attribute("hx-swap-oob", "beforeend"),
|
||||||
h.Id("connected-users"),
|
h.Id("connected-users"),
|
||||||
h.Class("flex flex-col"),
|
h.Class("flex flex-col"),
|
||||||
// This would be populated dynamically with connected users
|
// 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, "#", "-"))
|
id := fmt.Sprintf("connected-user-%s", strings.ReplaceAll(username, "#", "-"))
|
||||||
if remove {
|
if remove {
|
||||||
return h.Div(h.Id(id), h.Attribute("hx-swap-oob", "delete"))
|
return h.Div(h.Id(id), h.Attribute("hx-swap-oob", "delete"))
|
||||||
}
|
}
|
||||||
return h.Li(
|
return h.Li(
|
||||||
h.Id(id),
|
h.Id(id),
|
||||||
h.Class("truncate text-slate-700"),
|
h.ClassX("truncate text-slate-700", h.ClassMap{
|
||||||
|
"font-bold": isMe,
|
||||||
|
}),
|
||||||
h.Text(username),
|
h.Text(username),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,14 +28,16 @@ func ChatRoom(ctx *h.RequestContext) *h.Page {
|
||||||
h.HxOnWsClose(
|
h.HxOnWsClose(
|
||||||
js.EvalJs(fmt.Sprintf(`
|
js.EvalJs(fmt.Sprintf(`
|
||||||
const reason = e.detail.event.reason
|
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';
|
window.location.href = '/?roomId=%s';
|
||||||
} else if(e.detail.event.code === 1011) {
|
} else if(e.detail.event.code === 1011) {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
|
} else if (e.detail.event.code === 1008 || e.detail.event.code === 1006) {
|
||||||
|
window.location.href = '/?roomId=%s';
|
||||||
} else {
|
} else {
|
||||||
console.error('Connection closed:', e.detail.event)
|
console.error('Connection closed:', e.detail.event)
|
||||||
}
|
}
|
||||||
`, roomId)),
|
`, roomId, roomId)),
|
||||||
),
|
),
|
||||||
|
|
||||||
h.Class("flex flex-row min-h-screen bg-neutral-100"),
|
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.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.Div(
|
||||||
h.H3F("Connected Users", h.Class("text-lg font-bold")),
|
h.H3F("Connected Users", h.Class("text-lg font-bold")),
|
||||||
chat.ConnectedUsers(""),
|
chat.ConnectedUsers("", false),
|
||||||
),
|
),
|
||||||
h.A(
|
h.A(
|
||||||
h.Class("cursor-pointer"),
|
h.Class("cursor-pointer"),
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,6 @@ func Handle() http.HandlerFunc {
|
||||||
var v map[string]any
|
var v map[string]any
|
||||||
err = wsjson.Read(context.Background(), c, &v)
|
err = wsjson.Read(context.Background(), c, &v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to read message", slog.String("room_id", roomId))
|
|
||||||
manager.CloseWithError(sessionId, websocket.StatusInternalError, "failed to read message")
|
manager.CloseWithError(sessionId, websocket.StatusInternalError, "failed to read message")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,21 +28,25 @@ type SocketConnection struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SocketManager 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
|
listeners []chan SocketEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSocketManager() *SocketManager {
|
func NewSocketManager() *SocketManager {
|
||||||
return &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)) {
|
func (manager *SocketManager) ForEachSocket(roomId string, cb func(conn SocketConnection)) {
|
||||||
manager.sockets.Range(func(id string, conn SocketConnection) bool {
|
sockets, ok := manager.sockets.Load(roomId)
|
||||||
if conn.RoomId == roomId {
|
if !ok {
|
||||||
cb(conn)
|
return
|
||||||
}
|
}
|
||||||
|
sockets.Range(func(id string, conn SocketConnection) bool {
|
||||||
|
cb(conn)
|
||||||
return true
|
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) {
|
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,
|
Id: id,
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
RoomId: roomId,
|
RoomId: roomId,
|
||||||
})
|
})
|
||||||
s, ok := manager.sockets.Load(id)
|
|
||||||
|
s, ok := sockets.Load(id)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.dispatch(SocketEvent{
|
manager.dispatch(SocketEvent{
|
||||||
Id: s.Id,
|
Id: s.Id,
|
||||||
Type: ConnectedEvent,
|
Type: ConnectedEvent,
|
||||||
|
|
@ -122,26 +134,36 @@ func (manager *SocketManager) Disconnect(id string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *SocketManager) Get(id string) *SocketConnection {
|
func (manager *SocketManager) Get(id string) *SocketConnection {
|
||||||
conn, ok := manager.sockets.Load(id)
|
roomId, ok := manager.idToRoom.Load(id)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
sockets, ok := manager.sockets.Load(roomId)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
conn, ok := sockets.Load(id)
|
||||||
return &conn
|
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()
|
ctx := context.Background()
|
||||||
manager.sockets.Range(func(id string, conn SocketConnection) bool {
|
sockets, ok := manager.sockets.Load(roomId)
|
||||||
err := conn.Conn.Write(ctx, messageType, message)
|
|
||||||
if err != nil {
|
if !ok {
|
||||||
manager.Disconnect(id)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sockets.Range(func(id string, conn SocketConnection) bool {
|
||||||
|
if predicate(conn) {
|
||||||
|
conn.Conn.Write(ctx, messageType, message)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *SocketManager) BroadcastText(message string) {
|
func (manager *SocketManager) BroadcastText(roomId string, message string, predicate func(conn SocketConnection) bool) {
|
||||||
manager.Broadcast([]byte(message), websocket.MessageText)
|
manager.Broadcast(roomId, []byte(message), websocket.MessageText, predicate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *SocketManager) SendText(id string, message string) {
|
func (manager *SocketManager) SendText(id string, message string) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue