add error handling
This commit is contained in:
parent
5233bbb234
commit
784995728c
13 changed files with 127 additions and 42 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
framework/assets/dist/htmgo.js
vendored
4
framework/assets/dist/htmgo.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/).
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue