add error handling

This commit is contained in:
maddalax 2024-10-01 12:09:22 -05:00
parent 5233bbb234
commit 784995728c
13 changed files with 127 additions and 42 deletions

View file

@ -5,6 +5,7 @@ import (
"chat/ws" "chat/ws"
"context" "context"
"fmt" "fmt"
"github.com/coder/websocket"
"github.com/maddalax/htmgo/framework/h" "github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/service" "github.com/maddalax/htmgo/framework/service"
"time" "time"
@ -44,7 +45,15 @@ func (m *Manager) StartListener() {
} }
func (m *Manager) OnConnected(e ws.SocketEvent) { func (m *Manager) OnConnected(e ws.SocketEvent) {
room, _ := m.service.GetRoom(e.RoomId)
if room == nil {
m.socketManager.CloseWithError(e.Id, websocket.StatusPolicyViolation, "invalid room")
return
}
fmt.Printf("User %s connected to room %s\n", e.Id, e.RoomId) 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 {

View file

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/maddalax/htmgo/framework/h" "github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/hx"
"github.com/maddalax/htmgo/framework/js" "github.com/maddalax/htmgo/framework/js"
) )
@ -18,7 +17,24 @@ func ChatRoom(ctx *h.RequestContext) *h.Page {
h.TriggerChildren(), h.TriggerChildren(),
h.HxExtension("ws"), h.HxExtension("ws"),
), ),
h.Attribute("ws-connect", fmt.Sprintf("/ws/chat/%s", roomId)), h.Attribute("ws-connect", fmt.Sprintf("/ws/chat/%s", roomId)),
h.HxOnWsOpen(
js.ConsoleLog("Connected to chat room"),
),
h.HxOnWsClose(
js.EvalJs(`
const reason = e.detail.event.reason
if(['invalid room', 'no session'].includes(reason)) {
window.location.href = '/';
} else {
console.error('Connection closed:', e.detail.event)
}
`),
),
h.Class("flex flex-row min-h-screen bg-neutral-100"), h.Class("flex flex-row min-h-screen bg-neutral-100"),
// Sidebar for connected users // Sidebar for connected users
@ -28,11 +44,11 @@ func ChatRoom(ctx *h.RequestContext) *h.Page {
h.Div( h.Div(
h.Class("flex flex-col flex-grow gap-4 bg-white rounded p-4"), h.Class("flex flex-col flex-grow gap-4 bg-white rounded p-4"),
h.OnEvent("hx-on::ws-after-message", h.HxAfterWsMessage(
js.EvalJsOnSibling("#messages",
// language=JavaScript // language=JavaScript
js.EvalJsOnSibling("#messages", ` `element.scrollTop = element.scrollHeight;`),
element.scrollTop = element.scrollHeight; ),
`)),
// Chat Messages // Chat Messages
h.Div( h.Div(
@ -66,10 +82,9 @@ func MessageInput() *h.Element {
h.Class("p-4 rounded-md border border-slate-200 w-full"), h.Class("p-4 rounded-md border border-slate-200 w-full"),
h.Name("message"), h.Name("message"),
h.Placeholder("Type a message..."), h.Placeholder("Type a message..."),
h.HxBeforeWsSend( h.HxAfterWsSend(
js.SetValue(""), js.SetValue(""),
), ),
h.OnEvent(hx.KeyDownEvent, js.SubmitFormOnEnter()),
) )
} }

View file

@ -5,6 +5,7 @@ import (
"chat/components" "chat/components"
"github.com/maddalax/htmgo/framework/h" "github.com/maddalax/htmgo/framework/h"
"net/http" "net/http"
"time"
) )
func CreateOrJoinRoom(ctx *h.RequestContext) *h.Partial { func CreateOrJoinRoom(ctx *h.RequestContext) *h.Partial {
@ -29,6 +30,7 @@ func CreateOrJoinRoom(ctx *h.RequestContext) *h.Partial {
Name: "session_id", Name: "session_id",
Value: user.SessionID, Value: user.SessionID,
Path: "/", Path: "/",
Expires: time.Now().Add(24 * 30 * time.Hour),
} }
return h.SwapManyPartialWithHeaders( return h.SwapManyPartialWithHeaders(
ctx, ctx,

View file

@ -7,6 +7,7 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/maddalax/htmgo/framework/h" "github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/service" "github.com/maddalax/htmgo/framework/service"
"log/slog"
"net/http" "net/http"
) )
@ -14,31 +15,30 @@ func Handle() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
cc := r.Context().Value(h.RequestContextKey).(*h.RequestContext) cc := r.Context().Value(h.RequestContextKey).(*h.RequestContext)
sessionCookie, err := r.Cookie("session_id") sessionCookie, _ := r.Cookie("session_id")
cookies := r.Cookies()
println(cookies)
// no session
if err != nil {
return
}
c, err := websocket.Accept(w, r, nil) c, err := websocket.Accept(w, r, nil)
locator := cc.ServiceLocator()
manager := service.Get[SocketManager](locator)
if err != nil { if err != nil {
return return
} }
if sessionCookie == nil {
slog.Error("session cookie not found")
c.Close(websocket.StatusPolicyViolation, "no session")
return
}
locator := cc.ServiceLocator()
manager := service.Get[SocketManager](locator)
sessionId := sessionCookie.Value sessionId := sessionCookie.Value
roomId := chi.URLParam(r, "id") roomId := chi.URLParam(r, "id")
if roomId == "" { if roomId == "" {
manager.CloseWithError(sessionId, "invalid room") slog.Error("invalid room", slog.String("room_id", roomId))
manager.CloseWithError(sessionId, websocket.StatusPolicyViolation, "invalid room")
return return
} }
@ -52,7 +52,8 @@ 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 {
manager.CloseWithError(sessionId, "failed to read message") slog.Error("failed to read message", slog.String("room_id", roomId))
manager.CloseWithError(sessionId, websocket.StatusInternalError, "failed to read message")
return return
} }
if v != nil { if v != nil {

View file

@ -106,18 +106,18 @@ func (manager *SocketManager) OnClose(id string) {
manager.sockets.Delete(id) manager.sockets.Delete(id)
} }
func (manager *SocketManager) CloseWithError(id string, message string) { func (manager *SocketManager) CloseWithError(id string, code websocket.StatusCode, message string) {
conn := manager.Get(id) conn := manager.Get(id)
if conn != nil { if conn != nil {
defer manager.OnClose(id) go manager.OnClose(id)
conn.Conn.Close(websocket.StatusInternalError, message) conn.Conn.Close(code, message)
} }
} }
func (manager *SocketManager) Disconnect(id string) { func (manager *SocketManager) Disconnect(id string) {
conn := manager.Get(id) conn := manager.Get(id)
if conn != nil { if conn != nil {
defer manager.OnClose(id) go manager.OnClose(id)
_ = conn.Conn.CloseNow() _ = conn.Conn.CloseNow()
} }
} }

File diff suppressed because one or more lines are too long

View file

@ -4,6 +4,8 @@ function kebabEventName(str: string) {
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase() return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
} }
const ignoredEvents = ['htmx:beforeProcessNode', 'htmx:afterProcessNode', 'htmx:beforeSwap', 'htmx:afterSwap', 'htmx:beforeOnLoad', 'htmx:afterOnLoad', 'htmx:configRequest', 'htmx:configResponse', 'htmx:responseError'];
function makeEvent(eventName: string, detail: any) { function makeEvent(eventName: string, detail: any) {
let evt let evt
if (window.CustomEvent && typeof window.CustomEvent === 'function') { if (window.CustomEvent && typeof window.CustomEvent === 'function') {
@ -17,7 +19,9 @@ function makeEvent(eventName: string, detail: any) {
} }
function triggerChildren(target: HTMLElement, name: string, event: CustomEvent, triggered: Set<HTMLElement>) { function triggerChildren(target: HTMLElement, name: string, event: CustomEvent, triggered: Set<HTMLElement>) {
event.detail.meta = 'trigger-children'; if(ignoredEvents.includes(name)) {
return
}
if (target && target.children) { if (target && target.children) {
Array.from(target.children).forEach((e) => { Array.from(target.children).forEach((e) => {
const kehab = kebabEventName(name); const kehab = kebabEventName(name);
@ -25,6 +29,7 @@ function triggerChildren(target: HTMLElement, name: string, event: CustomEvent,
if (!triggered.has(e as HTMLElement)) { if (!triggered.has(e as HTMLElement)) {
if(e.hasAttribute(eventName)) { if(e.hasAttribute(eventName)) {
const newEvent = makeEvent(eventName.replace("hx-on::", "htmx:"), event.detail) const newEvent = makeEvent(eventName.replace("hx-on::", "htmx:"), event.detail)
newEvent.detail.meta = 'trigger-children'
e.dispatchEvent(newEvent) e.dispatchEvent(newEvent)
triggered.add(e as HTMLElement); triggered.add(e as HTMLElement);
} }

View file

@ -406,12 +406,13 @@ htmx.defineExtension('ws', {
// Try to create websockets when elements are processed // Try to create websockets when elements are processed
case 'htmx:beforeProcessNode': case 'htmx:beforeProcessNode':
forEach(queryAttributeOnThisOrChildren(parent, 'ws-connect'), (child) => { if(parent.hasAttribute("ws-connect")) {
ensureWebSocket(child); ensureWebSocket(parent as HTMLElement);
}); }
forEach(queryAttributeOnThisOrChildren(parent, 'ws-send'), (child) => {
ensureWebSocketSend(child); if(parent.hasAttribute("ws-send")) {
}); ensureWebSocketSend(parent as HTMLElement);
}
} }
}, },
}); });

View file

@ -86,6 +86,34 @@ func OnEvent(event hx.Event, cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(event, cmd...) return NewLifeCycle().OnEvent(event, cmd...)
} }
func HxBeforeWsMessage(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.WsBeforeMessageEvent, cmd...)
}
func HxAfterWsMessage(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.WsAfterMessageEvent, cmd...)
}
func OnSubmit(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.SubmitEvent, cmd...)
}
func HxOnWsError(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.WsErrorEvent, cmd...)
}
func HxOnWsClose(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.WsClosedEvent, cmd...)
}
func HxOnWsConnecting(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.WsConnectingEvent, cmd...)
}
func HxOnWsOpen(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.WsConnectedEvent, cmd...)
}
func HxBeforeWsSend(cmd ...Command) *LifeCycle { func HxBeforeWsSend(cmd ...Command) *LifeCycle {
return NewLifeCycle().HxBeforeWsSend(cmd...) return NewLifeCycle().HxBeforeWsSend(cmd...)
} }
@ -279,6 +307,16 @@ func EvalJs(js string) ComplexJsCommand {
return NewComplexJsCommand(js) return NewComplexJsCommand(js)
} }
func PreventDefault() SimpleJsCommand {
// language=JavaScript
return SimpleJsCommand{Command: "event.preventDefault()"}
}
func ConsoleLog(text string) SimpleJsCommand {
// language=JavaScript
return SimpleJsCommand{Command: fmt.Sprintf("console.log('%s')", text)}
}
func SetValue(value string) SimpleJsCommand { func SetValue(value string) SimpleJsCommand {
// language=JavaScript // language=JavaScript
return SimpleJsCommand{Command: fmt.Sprintf("this.value = '%s'", value)} return SimpleJsCommand{Command: fmt.Sprintf("this.value = '%s'", value)}
@ -288,7 +326,10 @@ func SubmitFormOnEnter() ComplexJsCommand {
// language=JavaScript // language=JavaScript
return EvalJs(` return EvalJs(`
if (event.code === 'Enter') { if (event.code === 'Enter') {
console.log('submitting form');
setTimeout(() => {
self.form.dispatchEvent(new Event('submit', { cancelable: true })); self.form.dispatchEvent(new Event('submit', { cancelable: true }));
}, 250)
} }
`) `)
} }

View file

@ -43,7 +43,8 @@ type RenderContext struct {
func (ctx *RenderContext) AddScript(funcName string, body string) { func (ctx *RenderContext) AddScript(funcName string, body string) {
script := fmt.Sprintf(` script := fmt.Sprintf(`
<script id="%s"> <script id="%s">
function %s(self) { function %s(self, event) {
let e = event;
%s %s
} }
</script>`, funcName, funcName, body) </script>`, funcName, funcName, body)
@ -223,10 +224,10 @@ func (l *LifeCycle) Render(context *RenderContext) {
for _, command := range commands { for _, command := range commands {
switch c := command.(type) { switch c := command.(type) {
case SimpleJsCommand: case SimpleJsCommand:
m[event] += fmt.Sprintf("%s;", c.Command) m[event] += fmt.Sprintf("var self=this;var e=event;%s;", c.Command)
case ComplexJsCommand: case ComplexJsCommand:
context.AddScript(c.TempFuncName, c.Command) context.AddScript(c.TempFuncName, c.Command)
m[event] += fmt.Sprintf("%s(this);", c.TempFuncName) m[event] += fmt.Sprintf("%s(this, event);", c.TempFuncName)
case *AttributeMapOrdered: case *AttributeMapOrdered:
c.Each(func(key string, value string) { c.Each(func(key string, value string) {
l.fromAttributeMap(event, key, value, context) l.fromAttributeMap(event, key, value, context)

View file

@ -110,6 +110,12 @@ const (
XhrProgressEvent Event = "htmx:xhr:progress" XhrProgressEvent Event = "htmx:xhr:progress"
BeforeWsSendEvent Event = "htmx:wsBeforeSend" BeforeWsSendEvent Event = "htmx:wsBeforeSend"
AfterWsSendEvent Event = "htmx:wsAfterSend" AfterWsSendEvent Event = "htmx:wsAfterSend"
WsConnectedEvent Event = "htmx:wsOpen"
WsConnectingEvent Event = "htmx:wsConnecting"
WsClosedEvent Event = "htmx:wsClose"
WsErrorEvent Event = "htmx:wsError"
WsBeforeMessageEvent Event = "htmx:wsBeforeMessage"
WsAfterMessageEvent Event = "htmx:wsAfterMessage"
// RevealedEvent Misc Events // RevealedEvent Misc Events
RevealedEvent Event = "revealed" RevealedEvent Event = "revealed"

View file

@ -21,7 +21,9 @@ var EvalJsOnParent = h.EvalJsOnParent
var SetClassOnSibling = h.SetClassOnSibling var SetClassOnSibling = h.SetClassOnSibling
var RemoveClassOnSibling = h.RemoveClassOnSibling var RemoveClassOnSibling = h.RemoveClassOnSibling
var Remove = h.Remove var Remove = h.Remove
var PreventDefault = h.PreventDefault
var EvalJs = h.EvalJs var EvalJs = h.EvalJs
var ConsoleLog = h.ConsoleLog
var SetValue = h.SetValue var SetValue = h.SetValue
var SubmitFormOnEnter = h.SubmitFormOnEnter var SubmitFormOnEnter = h.SubmitFormOnEnter
var InjectScript = h.InjectScript var InjectScript = h.InjectScript

View file

@ -42,6 +42,8 @@ OnClick(cmd ...Command) *LifeCycle
HxOnAfterSwap(cmd ...Command) *LifeCycle HxOnAfterSwap(cmd ...Command) *LifeCycle
HxOnLoad(cmd ...Command) *LifeCycle HxOnLoad(cmd ...Command) *LifeCycle
``` ```
**Note:** Each command you attach to the event handler will be passed 'self' and 'event' (if applicable) as arguments.
'self' is the current element, and 'event' is the event object.
If you use the OnEvent directly, event names may be any [HTML DOM](https://www.w3schools.com/jsref/dom_obj_event.asp) events, or any [HTMX events](https://htmx.org/events/). If you use the OnEvent directly, event names may be any [HTML DOM](https://www.w3schools.com/jsref/dom_obj_event.asp) events, or any [HTMX events](https://htmx.org/events/).