blumenjs 0.2.0 → 0.2.2
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.
- package/dist/cli/blumen.js +890 -67
- package/dist/cli/commands/build.js +60 -9
- package/dist/cli/commands/dev.js +1 -1
- package/dist/cli/commands/start.js +1 -1
- package/dist/templates/app/client/entry.tsx +5 -1
- package/dist/templates/app/pages/BlumenStarter.tsx +1 -1
- package/dist/templates/app/shared/DefaultDocument.tsx +67 -8
- package/dist/templates/app/shared/Link.tsx +60 -6
- package/dist/templates/app/shared/RouterContext.tsx +83 -20
- package/dist/templates/app/shared/router.ts +2 -1
- package/dist/templates/go-server/main.go +294 -4
- package/dist/templates/go-server/middleware.go +546 -0
- package/dist/templates/go-server/websocket.go +430 -0
- package/dist/templates/node-ssr/server.ts +467 -3
- package/dist/templates/scripts/generate-routes.ts +457 -17
- package/package.json +21 -7
|
@@ -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
|
+
}
|