nandi/oripublic Fork 0
55d4c9e2f7a9027b52846558166f42e50851523a
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

ws.go · 89 lines · 2.5 KBGo Blame HistoryRaw
✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS 2434cc7 k33g yesterday1package bridge
2
3import (
4 "context"
5 "net/http"
6
7 "github.com/coder/websocket"
8)
9
10// WebSocketHandler returns the HTTP handler that upgrades a request to a
11// WebSocket and attaches it to the bridge: replayed history first, then live
12// events, while incoming messages feed HandleIncoming.
13//
14// Example:
15//
16// mux.Handle("GET /ws", b.WebSocketHandler())
17func (b *Bridge) WebSocketHandler() http.Handler {
18 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19 conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
20 // ori serves localhost setups (possibly port-forwarded); the
21 // SPA and this endpoint share an origin in production, and the
22 // Vite dev server proxies /ws, so localhost patterns suffice.
23 OriginPatterns: []string{"localhost:*", "127.0.0.1:*"},
24 })
25 if err != nil {
26 b.logger.Warn("websocket accept failed", "error", err)
27 return
28 }
29 defer func() { _ = conn.CloseNow() }()
30 b.serveClient(r.Context(), conn)
31 })
32}
33
34// serveClient runs one client's session: replay, then a write pump for live
35// events alongside a read loop for incoming messages, until either side ends.
36func (b *Bridge) serveClient(ctx context.Context, conn *websocket.Conn) {
37 events, replay, unsubscribe := b.Subscribe()
38 defer unsubscribe()
39
40 if !writeEvents(ctx, conn, replay) {
41 return
42 }
43
44 writeDone := make(chan struct{})
45 go func() {
46 defer close(writeDone)
47 pumpEvents(ctx, conn, events)
48 }()
49
50 b.readLoop(ctx, conn)
51
52 // Stop the write pump by closing its channel, then close cleanly.
53 unsubscribe()
54 <-writeDone
55 _ = conn.Close(websocket.StatusNormalClosure, "")
56}
57
58// writeEvents sends a batch of events; false means the connection is gone.
59func writeEvents(ctx context.Context, conn *websocket.Conn, events [][]byte) bool {
60 for _, event := range events {
61 if err := conn.Write(ctx, websocket.MessageText, event); err != nil {
62 return false
63 }
64 }
65 return true
66}
67
68// pumpEvents forwards live bridge events to the socket until the
69// subscription channel closes or a write fails.
70func pumpEvents(ctx context.Context, conn *websocket.Conn, events <-chan []byte) {
71 for event := range events {
72 if err := conn.Write(ctx, websocket.MessageText, event); err != nil {
73 return
74 }
75 }
76}
77
78// readLoop feeds client messages to the bridge until the socket closes.
79func (b *Bridge) readLoop(ctx context.Context, conn *websocket.Conn) {
80 for {
81 kind, data, err := conn.Read(ctx)
82 if err != nil {
83 return
84 }
85 if kind == websocket.MessageText {
86 b.HandleIncoming(ctx, data)
87 }
88 }
89}