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"
"context"
"fmt"
"github.com/coder/websocket"
"github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/service"
"time"
@ -44,7 +45,15 @@ func (m *Manager) StartListener() {
}
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)
user, err := m.queries.GetUserBySessionId(context.Background(), e.Id)
if err != nil {

View file

@ -5,7 +5,6 @@ import (
"fmt"
"github.com/go-chi/chi/v5"
"github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/hx"
"github.com/maddalax/htmgo/framework/js"
)
@ -18,7 +17,24 @@ func ChatRoom(ctx *h.RequestContext) *h.Page {
h.TriggerChildren(),
h.HxExtension("ws"),
),
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"),
// Sidebar for connected users
@ -28,11 +44,11 @@ func ChatRoom(ctx *h.RequestContext) *h.Page {
h.Div(
h.Class("flex flex-col flex-grow gap-4 bg-white rounded p-4"),
h.OnEvent("hx-on::ws-after-message",
// language=JavaScript
js.EvalJsOnSibling("#messages", `
element.scrollTop = element.scrollHeight;
`)),
h.HxAfterWsMessage(
js.EvalJsOnSibling("#messages",
// language=JavaScript
`element.scrollTop = element.scrollHeight;`),
),
// Chat Messages
h.Div(
@ -66,10 +82,9 @@ func MessageInput() *h.Element {
h.Class("p-4 rounded-md border border-slate-200 w-full"),
h.Name("message"),
h.Placeholder("Type a message..."),
h.HxBeforeWsSend(
h.HxAfterWsSend(
js.SetValue(""),
),
h.OnEvent(hx.KeyDownEvent, js.SubmitFormOnEnter()),
)
}

View file

@ -5,6 +5,7 @@ import (
"chat/components"
"github.com/maddalax/htmgo/framework/h"
"net/http"
"time"
)
func CreateOrJoinRoom(ctx *h.RequestContext) *h.Partial {
@ -26,9 +27,10 @@ func CreateOrJoinRoom(ctx *h.RequestContext) *h.Partial {
var redirect = func(path string) *h.Partial {
cookie := &http.Cookie{
Name: "session_id",
Value: user.SessionID,
Path: "/",
Name: "session_id",
Value: user.SessionID,
Path: "/",
Expires: time.Now().Add(24 * 30 * time.Hour),
}
return h.SwapManyPartialWithHeaders(
ctx,

View file

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

View file

@ -106,18 +106,18 @@ func (manager *SocketManager) OnClose(id string) {
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)
if conn != nil {
defer manager.OnClose(id)
conn.Conn.Close(websocket.StatusInternalError, message)
go manager.OnClose(id)
conn.Conn.Close(code, message)
}
}
func (manager *SocketManager) Disconnect(id string) {
conn := manager.Get(id)
if conn != nil {
defer manager.OnClose(id)
go manager.OnClose(id)
_ = 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()
}
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) {
let evt
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>) {
event.detail.meta = 'trigger-children';
if(ignoredEvents.includes(name)) {
return
}
if (target && target.children) {
Array.from(target.children).forEach((e) => {
const kehab = kebabEventName(name);
@ -25,6 +29,7 @@ function triggerChildren(target: HTMLElement, name: string, event: CustomEvent,
if (!triggered.has(e as HTMLElement)) {
if(e.hasAttribute(eventName)) {
const newEvent = makeEvent(eventName.replace("hx-on::", "htmx:"), event.detail)
newEvent.detail.meta = 'trigger-children'
e.dispatchEvent(newEvent)
triggered.add(e as HTMLElement);
}

View file

@ -406,12 +406,13 @@ htmx.defineExtension('ws', {
// Try to create websockets when elements are processed
case 'htmx:beforeProcessNode':
forEach(queryAttributeOnThisOrChildren(parent, 'ws-connect'), (child) => {
ensureWebSocket(child);
});
forEach(queryAttributeOnThisOrChildren(parent, 'ws-send'), (child) => {
ensureWebSocketSend(child);
});
if(parent.hasAttribute("ws-connect")) {
ensureWebSocket(parent as HTMLElement);
}
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...)
}
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 {
return NewLifeCycle().HxBeforeWsSend(cmd...)
}
@ -279,6 +307,16 @@ func EvalJs(js string) ComplexJsCommand {
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 {
// language=JavaScript
return SimpleJsCommand{Command: fmt.Sprintf("this.value = '%s'", value)}
@ -288,7 +326,10 @@ func SubmitFormOnEnter() ComplexJsCommand {
// language=JavaScript
return EvalJs(`
if (event.code === 'Enter') {
self.form.dispatchEvent(new Event('submit', { cancelable: true }));
console.log('submitting form');
setTimeout(() => {
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) {
script := fmt.Sprintf(`
<script id="%s">
function %s(self) {
function %s(self, event) {
let e = event;
%s
}
</script>`, funcName, funcName, body)
@ -223,10 +224,10 @@ func (l *LifeCycle) Render(context *RenderContext) {
for _, command := range commands {
switch c := command.(type) {
case SimpleJsCommand:
m[event] += fmt.Sprintf("%s;", c.Command)
m[event] += fmt.Sprintf("var self=this;var e=event;%s;", c.Command)
case ComplexJsCommand:
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:
c.Each(func(key string, value string) {
l.fromAttributeMap(event, key, value, context)

View file

@ -110,6 +110,12 @@ const (
XhrProgressEvent Event = "htmx:xhr:progress"
BeforeWsSendEvent Event = "htmx:wsBeforeSend"
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 Event = "revealed"

View file

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

View file

@ -42,6 +42,8 @@ OnClick(cmd ...Command) *LifeCycle
HxOnAfterSwap(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/).