blumenjs 0.1.7 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,430 @@
1
+ package main
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "log"
7
+ "net/http"
8
+ "sync"
9
+ "time"
10
+
11
+ "github.com/gorilla/websocket"
12
+ )
13
+
14
+ // ─── WebSocket Hub ─────────────────────────────────────────────
15
+ // Manages all WebSocket connections with room/channel support.
16
+ // Each connection runs in its own goroutine — Go handles 100K+
17
+ // concurrent connections with minimal memory overhead.
18
+
19
+ // WSMessage represents a message sent over WebSocket
20
+ type WSMessage struct {
21
+ Type string `json:"type"` // Message type (e.g. "chat", "notification", "ping")
22
+ Room string `json:"room,omitempty"` // Target room (empty = broadcast to all)
23
+ Payload json.RawMessage `json:"payload,omitempty"` // Arbitrary JSON payload
24
+ Sender string `json:"sender,omitempty"` // Connection ID of the sender
25
+ }
26
+
27
+ // WSClient represents a single WebSocket connection
28
+ type WSClient struct {
29
+ hub *WSHub
30
+ conn *websocket.Conn
31
+ send chan []byte
32
+ id string
33
+ rooms map[string]bool
34
+ mu sync.RWMutex
35
+ }
36
+
37
+ // WSHub manages all active WebSocket clients and rooms
38
+ type WSHub struct {
39
+ clients map[*WSClient]bool
40
+ rooms map[string]map[*WSClient]bool
41
+ broadcast chan []byte
42
+ register chan *WSClient
43
+ unregister chan *WSClient
44
+ mu sync.RWMutex
45
+
46
+ // OnConnect is called when a client connects. Return false to reject.
47
+ OnConnect func(client *WSClient, r *http.Request) bool
48
+
49
+ // OnMessage is called when a message is received from a client.
50
+ // Return a response message or nil to not respond.
51
+ OnMessage func(client *WSClient, msg WSMessage) *WSMessage
52
+
53
+ // OnDisconnect is called when a client disconnects.
54
+ OnDisconnect func(client *WSClient)
55
+ }
56
+
57
+ // NewWSHub creates a new WebSocket hub
58
+ func NewWSHub() *WSHub {
59
+ return &WSHub{
60
+ clients: make(map[*WSClient]bool),
61
+ rooms: make(map[string]map[*WSClient]bool),
62
+ broadcast: make(chan []byte, 256),
63
+ register: make(chan *WSClient),
64
+ unregister: make(chan *WSClient),
65
+ }
66
+ }
67
+
68
+ // Run starts the hub's event loop. Call as a goroutine.
69
+ func (h *WSHub) Run() {
70
+ for {
71
+ select {
72
+ case client := <-h.register:
73
+ h.mu.Lock()
74
+ h.clients[client] = true
75
+ h.mu.Unlock()
76
+ log.Printf("🔌 WebSocket client connected: %s (total: %d)", client.id, len(h.clients))
77
+
78
+ case client := <-h.unregister:
79
+ h.mu.Lock()
80
+ if _, ok := h.clients[client]; ok {
81
+ delete(h.clients, client)
82
+ close(client.send)
83
+
84
+ // Remove from all rooms
85
+ client.mu.RLock()
86
+ for room := range client.rooms {
87
+ if clients, ok := h.rooms[room]; ok {
88
+ delete(clients, client)
89
+ if len(clients) == 0 {
90
+ delete(h.rooms, room)
91
+ }
92
+ }
93
+ }
94
+ client.mu.RUnlock()
95
+ }
96
+ h.mu.Unlock()
97
+ if h.OnDisconnect != nil {
98
+ h.OnDisconnect(client)
99
+ }
100
+ log.Printf("🔌 WebSocket client disconnected: %s (total: %d)", client.id, len(h.clients))
101
+
102
+ case message := <-h.broadcast:
103
+ h.mu.RLock()
104
+ for client := range h.clients {
105
+ select {
106
+ case client.send <- message:
107
+ default:
108
+ // Client buffer full — disconnect
109
+ close(client.send)
110
+ delete(h.clients, client)
111
+ }
112
+ }
113
+ h.mu.RUnlock()
114
+ }
115
+ }
116
+ }
117
+
118
+ // BroadcastJSON sends a WSMessage to all connected clients
119
+ func (h *WSHub) BroadcastJSON(msg WSMessage) {
120
+ data, err := json.Marshal(msg)
121
+ if err != nil {
122
+ log.Printf("WebSocket broadcast marshal error: %v", err)
123
+ return
124
+ }
125
+ h.broadcast <- data
126
+ }
127
+
128
+ // SendToRoom sends a message to all clients in a specific room
129
+ func (h *WSHub) SendToRoom(room string, msg WSMessage) {
130
+ data, err := json.Marshal(msg)
131
+ if err != nil {
132
+ return
133
+ }
134
+
135
+ h.mu.RLock()
136
+ clients, ok := h.rooms[room]
137
+ h.mu.RUnlock()
138
+
139
+ if !ok {
140
+ return
141
+ }
142
+
143
+ for client := range clients {
144
+ select {
145
+ case client.send <- data:
146
+ default:
147
+ close(client.send)
148
+ h.mu.Lock()
149
+ delete(h.clients, client)
150
+ delete(clients, client)
151
+ h.mu.Unlock()
152
+ }
153
+ }
154
+ }
155
+
156
+ // SendToClient sends a message to a specific client by ID
157
+ func (h *WSHub) SendToClient(clientID string, msg WSMessage) {
158
+ data, err := json.Marshal(msg)
159
+ if err != nil {
160
+ return
161
+ }
162
+
163
+ h.mu.RLock()
164
+ defer h.mu.RUnlock()
165
+
166
+ for client := range h.clients {
167
+ if client.id == clientID {
168
+ select {
169
+ case client.send <- data:
170
+ default:
171
+ }
172
+ return
173
+ }
174
+ }
175
+ }
176
+
177
+ // ClientCount returns the number of connected clients
178
+ func (h *WSHub) ClientCount() int {
179
+ h.mu.RLock()
180
+ defer h.mu.RUnlock()
181
+ return len(h.clients)
182
+ }
183
+
184
+ // RoomCount returns the number of clients in a specific room
185
+ func (h *WSHub) RoomCount(room string) int {
186
+ h.mu.RLock()
187
+ defer h.mu.RUnlock()
188
+ if clients, ok := h.rooms[room]; ok {
189
+ return len(clients)
190
+ }
191
+ return 0
192
+ }
193
+
194
+ // ─── Client Methods ────────────────────────────────────────────
195
+
196
+ // JoinRoom adds the client to a room
197
+ func (c *WSClient) JoinRoom(room string) {
198
+ c.mu.Lock()
199
+ c.rooms[room] = true
200
+ c.mu.Unlock()
201
+
202
+ c.hub.mu.Lock()
203
+ if _, ok := c.hub.rooms[room]; !ok {
204
+ c.hub.rooms[room] = make(map[*WSClient]bool)
205
+ }
206
+ c.hub.rooms[room][c] = true
207
+ c.hub.mu.Unlock()
208
+
209
+ log.Printf("🔌 Client %s joined room: %s", c.id, room)
210
+ }
211
+
212
+ // LeaveRoom removes the client from a room
213
+ func (c *WSClient) LeaveRoom(room string) {
214
+ c.mu.Lock()
215
+ delete(c.rooms, room)
216
+ c.mu.Unlock()
217
+
218
+ c.hub.mu.Lock()
219
+ if clients, ok := c.hub.rooms[room]; ok {
220
+ delete(clients, c)
221
+ if len(clients) == 0 {
222
+ delete(c.hub.rooms, room)
223
+ }
224
+ }
225
+ c.hub.mu.Unlock()
226
+
227
+ log.Printf("🔌 Client %s left room: %s", c.id, room)
228
+ }
229
+
230
+ // Send sends a message directly to this client
231
+ func (c *WSClient) Send(msg WSMessage) {
232
+ data, err := json.Marshal(msg)
233
+ if err != nil {
234
+ return
235
+ }
236
+ select {
237
+ case c.send <- data:
238
+ default:
239
+ }
240
+ }
241
+
242
+ // ID returns the client's unique identifier
243
+ func (c *WSClient) ID() string {
244
+ return c.id
245
+ }
246
+
247
+ // ─── WebSocket Upgrader ────────────────────────────────────────
248
+
249
+ var upgrader = websocket.Upgrader{
250
+ ReadBufferSize: 1024,
251
+ WriteBufferSize: 1024,
252
+ // Allow all origins in development. In production, configure this.
253
+ CheckOrigin: func(r *http.Request) bool {
254
+ return true
255
+ },
256
+ }
257
+
258
+ // WebSocket read/write configuration
259
+ const (
260
+ writeWait = 10 * time.Second
261
+ pongWait = 60 * time.Second
262
+ pingPeriod = (pongWait * 9) / 10
263
+ maxMessageSize = 65536 // 64KB
264
+ )
265
+
266
+ // readPump pumps messages from the WebSocket connection to the hub.
267
+ func (c *WSClient) readPump() {
268
+ defer func() {
269
+ c.hub.unregister <- c
270
+ c.conn.Close()
271
+ }()
272
+
273
+ c.conn.SetReadLimit(maxMessageSize)
274
+ c.conn.SetReadDeadline(time.Now().Add(pongWait))
275
+ c.conn.SetPongHandler(func(string) error {
276
+ c.conn.SetReadDeadline(time.Now().Add(pongWait))
277
+ return nil
278
+ })
279
+
280
+ for {
281
+ _, rawMsg, err := c.conn.ReadMessage()
282
+ if err != nil {
283
+ if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
284
+ log.Printf("WebSocket read error: %v", err)
285
+ }
286
+ break
287
+ }
288
+
289
+ // Parse the incoming message
290
+ var msg WSMessage
291
+ if err := json.Unmarshal(rawMsg, &msg); err != nil {
292
+ log.Printf("WebSocket message parse error: %v", err)
293
+ continue
294
+ }
295
+
296
+ msg.Sender = c.id
297
+
298
+ // Handle built-in message types
299
+ switch msg.Type {
300
+ case "join":
301
+ var room string
302
+ json.Unmarshal(msg.Payload, &room)
303
+ if room != "" {
304
+ c.JoinRoom(room)
305
+ }
306
+ continue
307
+
308
+ case "leave":
309
+ var room string
310
+ json.Unmarshal(msg.Payload, &room)
311
+ if room != "" {
312
+ c.LeaveRoom(room)
313
+ }
314
+ continue
315
+
316
+ case "ping":
317
+ c.Send(WSMessage{Type: "pong"})
318
+ continue
319
+ }
320
+
321
+ // Call the user-defined message handler
322
+ if c.hub.OnMessage != nil {
323
+ if response := c.hub.OnMessage(c, msg); response != nil {
324
+ c.Send(*response)
325
+ }
326
+ }
327
+
328
+ // If the message targets a room, broadcast to that room
329
+ if msg.Room != "" {
330
+ c.hub.SendToRoom(msg.Room, msg)
331
+ }
332
+ }
333
+ }
334
+
335
+ // writePump pumps messages from the hub to the WebSocket connection.
336
+ func (c *WSClient) writePump() {
337
+ ticker := time.NewTicker(pingPeriod)
338
+ defer func() {
339
+ ticker.Stop()
340
+ c.conn.Close()
341
+ }()
342
+
343
+ for {
344
+ select {
345
+ case message, ok := <-c.send:
346
+ c.conn.SetWriteDeadline(time.Now().Add(writeWait))
347
+ if !ok {
348
+ // Hub closed the channel
349
+ c.conn.WriteMessage(websocket.CloseMessage, []byte{})
350
+ return
351
+ }
352
+
353
+ w, err := c.conn.NextWriter(websocket.TextMessage)
354
+ if err != nil {
355
+ return
356
+ }
357
+ w.Write(message)
358
+
359
+ // Drain queued messages into the current write
360
+ n := len(c.send)
361
+ for i := 0; i < n; i++ {
362
+ w.Write([]byte("\n"))
363
+ w.Write(<-c.send)
364
+ }
365
+
366
+ if err := w.Close(); err != nil {
367
+ return
368
+ }
369
+
370
+ case <-ticker.C:
371
+ c.conn.SetWriteDeadline(time.Now().Add(writeWait))
372
+ if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
373
+ return
374
+ }
375
+ }
376
+ }
377
+ }
378
+
379
+ // ─── HTTP Handler ──────────────────────────────────────────────
380
+
381
+ var clientIDCounter uint64
382
+ var clientIDMu sync.Mutex
383
+
384
+ func nextClientID() string {
385
+ clientIDMu.Lock()
386
+ clientIDCounter++
387
+ id := clientIDCounter
388
+ clientIDMu.Unlock()
389
+ return log.Prefix() + "ws-" + time.Now().Format("150405") + "-" + fmt.Sprintf("%d", id)
390
+ }
391
+
392
+ // WSHandler returns an HTTP handler that upgrades to WebSocket
393
+ func WSHandler(hub *WSHub) http.HandlerFunc {
394
+ return func(w http.ResponseWriter, r *http.Request) {
395
+ conn, err := upgrader.Upgrade(w, r, nil)
396
+ if err != nil {
397
+ log.Printf("WebSocket upgrade error: %v", err)
398
+ return
399
+ }
400
+
401
+ client := &WSClient{
402
+ hub: hub,
403
+ conn: conn,
404
+ send: make(chan []byte, 256),
405
+ id: nextClientID(),
406
+ rooms: make(map[string]bool),
407
+ }
408
+
409
+ // Check if the connection should be accepted
410
+ if hub.OnConnect != nil && !hub.OnConnect(client, r) {
411
+ conn.WriteMessage(websocket.CloseMessage,
412
+ websocket.FormatCloseMessage(websocket.ClosePolicyViolation, "connection rejected"))
413
+ conn.Close()
414
+ return
415
+ }
416
+
417
+ hub.register <- client
418
+
419
+ // Send the client its ID
420
+ welcome := WSMessage{
421
+ Type: "connected",
422
+ Payload: json.RawMessage(`"` + client.id + `"`),
423
+ }
424
+ client.Send(welcome)
425
+
426
+ // Start read/write pumps in goroutines
427
+ go client.writePump()
428
+ go client.readPump()
429
+ }
430
+ }