feat(wasm): add production build with stripped logging

- Add //go:build !prod tags to main_js.go and inject.go - Create main_js_prod.go with //go:build prod tag - Remove zerolog dependency from production build - Make InjectHTML a no-op in production (handled by service worker) This reduces WASM binary size and runtime overhead by excluding debug logging and HTML parsing from production builds.

cognitive committed Dec 30, 2025 at 00:22 UTC b2dcb20868e3b6331b8a239fe553c14487bb4204
3 files changed +871
cmd/webclient/inject.go
+2
@@ -1,3 +1,5 @@
1 +//go:build !prod
2 +
3 package main
4
5 import (
cmd/webclient/main_js.go
+2
@@ -1,3 +1,5 @@
1 +//go:build !prod
2 +
3 package main
4
5 import (
cmd/webclient/main_js_prod.go new
+867
@@ -0,0 +1,867 @@
1 +//go:build prod
2 +
3 +package main
4 +
5 +import (
6 + "context"
7 + "crypto/rand"
8 + "encoding/base64"
9 + "encoding/hex"
10 + "encoding/json"
11 + "fmt"
12 + "io"
13 + "net"
14 + "net/http"
15 + "net/url"
16 + "runtime"
17 + "strings"
18 + "sync"
19 + "syscall/js"
20 + "time"
21 +
22 + "github.com/gorilla/websocket"
23 + "golang.org/x/net/idna"
24 + "gosuda.org/portal/cmd/webclient/httpjs"
25 + "gosuda.org/portal/portal/core/cryptoops"
26 + "gosuda.org/portal/sdk"
27 + "gosuda.org/portal/utils"
28 +)
29 +
30 +// Production build: no logging overhead
31 +
32 +var (
33 + client *sdk.Client
34 +
35 + // SDK connection manager for Service Worker messaging
36 + sdkConnections = make(map[string]io.ReadWriteCloser)
37 + sdkConnectionsMu sync.RWMutex
38 +
39 + // Reusable credential for HTTP connections (enables Keep-Alive)
40 + dialerCredential *cryptoops.Credential
41 +
42 + // DNS cache for lease name -> lease ID mapping
43 + dnsCache sync.Map // map[string]*dnsCacheEntry
44 + dnsCacheTTL = 5 * time.Minute
45 +)
46 +
47 +type dnsCacheEntry struct {
48 + leaseID string
49 + expiresAt time.Time
50 +}
51 +
52 +// getBootstrapServers retrieves bootstrap servers from global JavaScript variable
53 +func getBootstrapServers() []string {
54 + bootstrapsValue := js.Global().Get("__BOOTSTRAP_SERVERS__")
55 +
56 + if bootstrapsValue.IsUndefined() || bootstrapsValue.IsNull() {
57 + return []string{"ws://localhost:4017/relay"}
58 + }
59 +
60 + if bootstrapsValue.Type() == js.TypeString {
61 + bootstrapsStr := bootstrapsValue.String()
62 + if bootstrapsStr == "" {
63 + return []string{"ws://localhost:4017/relay"}
64 + }
65 + servers := strings.Split(bootstrapsStr, ",")
66 + for i := range servers {
67 + servers[i] = strings.TrimSpace(servers[i])
68 + }
69 + return servers
70 + }
71 +
72 + if bootstrapsValue.Type() == js.TypeObject && bootstrapsValue.Length() > 0 {
73 + servers := make([]string, bootstrapsValue.Length())
74 + for i := 0; i < bootstrapsValue.Length(); i++ {
75 + servers[i] = bootstrapsValue.Index(i).String()
76 + }
77 + return servers
78 + }
79 +
80 + return []string{"ws://localhost:4017/relay"}
81 +}
82 +
83 +func lookupDNSCache(name string) (string, bool) {
84 + if entry, ok := dnsCache.Load(name); ok {
85 + cached := entry.(*dnsCacheEntry)
86 + if time.Now().Before(cached.expiresAt) {
87 + return cached.leaseID, true
88 + }
89 + dnsCache.Delete(name)
90 + }
91 + return "", false
92 +}
93 +
94 +func storeDNSCache(name, leaseID string) {
95 + dnsCache.Store(name, &dnsCacheEntry{
96 + leaseID: leaseID,
97 + expiresAt: time.Now().Add(dnsCacheTTL),
98 + })
99 +}
100 +
101 +func isValidUpgradeRequest(req []byte) bool {
102 + if len(req) < 20 {
103 + return false
104 + }
105 + s := string(req)
106 + if !strings.HasPrefix(s, "GET ") {
107 + return false
108 + }
109 + if !strings.HasSuffix(s, "\r\n\r\n") {
110 + return false
111 + }
112 + if !strings.Contains(strings.ToLower(s), "upgrade:") {
113 + return false
114 + }
115 + return true
116 +}
117 +
118 +var rdDialer = func(ctx context.Context, network, address string) (net.Conn, error) {
119 + address = strings.TrimSuffix(address, ":80")
120 + address = strings.TrimSuffix(address, ":443")
121 +
122 + decodedAddr, err := url.QueryUnescape(address)
123 + if err != nil {
124 + decodedAddr = address
125 + }
126 + address = decodedAddr
127 +
128 + unicodeAddr, err := idna.ToUnicode(address)
129 + if err != nil {
130 + unicodeAddr = address
131 + }
132 + address = unicodeAddr
133 +
134 + if cachedID, ok := lookupDNSCache(address); ok {
135 + address = cachedID
136 + } else {
137 + lease, err := client.LookupName(address)
138 + if err == nil && lease != nil {
139 + leaseID := lease.Identity.Id
140 + storeDNSCache(unicodeAddr, leaseID)
141 + address = leaseID
142 + }
143 + }
144 +
145 + conn, err := client.Dial(dialerCredential, address, "http/1.1")
146 + if err != nil {
147 + return nil, err
148 + }
149 +
150 + return conn, nil
151 +}
152 +
153 +var httpClient = &http.Client{
154 + Timeout: time.Second * 30,
155 + Transport: &http.Transport{
156 + MaxIdleConns: 1000,
157 + MaxIdleConnsPerHost: 100,
158 + DialContext: rdDialer,
159 + },
160 +}
161 +
162 +type Proxy struct {
163 + wsManager *WebSocketManager
164 +}
165 +
166 +type WebSocketManager struct {
167 + connections sync.Map
168 +}
169 +
170 +type WSConnection struct {
171 + id string
172 + conn *websocket.Conn
173 + messageChan chan wsMessage
174 + closeChan chan struct{}
175 + closeOnce sync.Once
176 + mu sync.Mutex
177 + messageQueue []StreamMessage
178 + queueMu sync.Mutex
179 + isClosed bool
180 +}
181 +
182 +type wsMessage struct {
183 + data []byte
184 + isText bool
185 +}
186 +
187 +type ConnectRequest struct {
188 + URL string `json:"url"`
189 + Protocols []string `json:"protocols"`
190 +}
191 +
192 +type ConnectResponse struct {
193 + ConnID string `json:"connId"`
194 + Protocol string `json:"protocol"`
195 +}
196 +
197 +type SendRequest struct {
198 + Type string `json:"type"`
199 + Data string `json:"data,omitempty"`
200 + Code int `json:"code,omitempty"`
201 + Reason string `json:"reason,omitempty"`
202 +}
203 +
204 +type StreamMessage struct {
205 + Type string `json:"type"`
206 + Data string `json:"data,omitempty"`
207 + MessageType string `json:"messageType,omitempty"`
208 + Code int `json:"code,omitempty"`
209 + Reason string `json:"reason,omitempty"`
210 +}
211 +
212 +func NewWebSocketManager() *WebSocketManager {
213 + return &WebSocketManager{}
214 +}
215 +
216 +func generateConnID() string {
217 + b := make([]byte, 16)
218 + rand.Read(b)
219 + return hex.EncodeToString(b)
220 +}
221 +
222 +func (m *WebSocketManager) CreateConnection(uri string, protocols []string) (*WSConnection, string, error) {
223 + u, err := url.Parse(uri)
224 + if err != nil {
225 + return nil, "", err
226 + }
227 + id := getLeaseID(u.Hostname())
228 +
229 + u.Scheme = "ws"
230 + u.Host = id
231 +
232 + dialer := websocket.Dialer{
233 + NetDialContext: rdDialer,
234 + Subprotocols: protocols,
235 + }
236 +
237 + conn, resp, err := dialer.Dial(u.String(), nil)
238 + if err != nil {
239 + return nil, "", err
240 + }
241 +
242 + negotiatedProtocol := ""
243 + if resp != nil && resp.Header != nil {
244 + negotiatedProtocol = resp.Header.Get("Sec-WebSocket-Protocol")
245 + }
246 +
247 + wsConn := &WSConnection{
248 + id: generateConnID(),
249 + conn: conn,
250 + messageChan: make(chan wsMessage, 100),
251 + closeChan: make(chan struct{}),
252 + messageQueue: make([]StreamMessage, 0),
253 + }
254 +
255 + m.connections.Store(wsConn.id, wsConn)
256 +
257 + go wsConn.receiveMessages()
258 + go wsConn.manageQueue()
259 +
260 + return wsConn, negotiatedProtocol, nil
261 +}
262 +
263 +func (m *WebSocketManager) GetConnection(id string) (*WSConnection, bool) {
264 + conn, ok := m.connections.Load(id)
265 + if !ok {
266 + return nil, false
267 + }
268 + return conn.(*WSConnection), true
269 +}
270 +
271 +func (m *WebSocketManager) RemoveConnection(id string) {
272 + m.connections.Delete(id)
273 +}
274 +
275 +func (c *WSConnection) receiveMessages() {
276 + defer c.Close()
277 +
278 + for {
279 + messageType, msg, err := c.conn.ReadMessage()
280 + if err != nil {
281 + c.queueMu.Lock()
282 + c.isClosed = true
283 + c.queueMu.Unlock()
284 + return
285 + }
286 +
287 + if messageType != websocket.BinaryMessage && messageType != websocket.TextMessage {
288 + continue
289 + }
290 +
291 + wsMsg := wsMessage{
292 + data: msg,
293 + isText: messageType == websocket.TextMessage,
294 + }
295 +
296 + select {
297 + case c.messageChan <- wsMsg:
298 + case <-c.closeChan:
299 + return
300 + }
301 + }
302 +}
303 +
304 +func (c *WSConnection) manageQueue() {
305 + for {
306 + select {
307 + case msg := <-c.messageChan:
308 + c.queueMu.Lock()
309 +
310 + messageType := "binary"
311 + if msg.isText {
312 + messageType = "text"
313 + }
314 +
315 + streamMsg := StreamMessage{
316 + Type: "message",
317 + Data: base64.StdEncoding.EncodeToString(msg.data),
318 + MessageType: messageType,
319 + }
320 + c.messageQueue = append(c.messageQueue, streamMsg)
321 + c.queueMu.Unlock()
322 +
323 + case <-c.closeChan:
324 + c.queueMu.Lock()
325 + c.isClosed = true
326 + c.messageQueue = append(c.messageQueue, StreamMessage{
327 + Type: "close",
328 + Code: 1000,
329 + Reason: "Connection closed",
330 + })
331 + c.queueMu.Unlock()
332 + return
333 + }
334 + }
335 +}
336 +
337 +func (c *WSConnection) GetMessages() []StreamMessage {
338 + c.queueMu.Lock()
339 + defer c.queueMu.Unlock()
340 +
341 + messages := make([]StreamMessage, len(c.messageQueue))
342 + copy(messages, c.messageQueue)
343 + c.messageQueue = c.messageQueue[:0]
344 +
345 + return messages
346 +}
347 +
348 +func (c *WSConnection) IsClosed() bool {
349 + c.queueMu.Lock()
350 + defer c.queueMu.Unlock()
351 + return c.isClosed
352 +}
353 +
354 +func (c *WSConnection) Send(data []byte, isText bool) error {
355 + c.mu.Lock()
356 + defer c.mu.Unlock()
357 +
358 + select {
359 + case <-c.closeChan:
360 + return fmt.Errorf("connection closed")
361 + default:
362 + messageType := websocket.BinaryMessage
363 + if isText {
364 + messageType = websocket.TextMessage
365 + }
366 + return c.conn.WriteMessage(messageType, data)
367 + }
368 +}
369 +
370 +func (c *WSConnection) Close() {
371 + c.closeOnce.Do(func() {
372 + close(c.closeChan)
373 + c.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
374 + c.conn.Close()
375 + })
376 +}
377 +
378 +func getLeaseID(hostname string) string {
379 + decoded, err := url.QueryUnescape(hostname)
380 + if err != nil {
381 + decoded = hostname
382 + }
383 +
384 + decoded = strings.ToLower(decoded)
385 +
386 + host, err := idna.ToUnicode(decoded)
387 + if err != nil {
388 + host = decoded
389 + }
390 +
391 + id := strings.Split(host, ".")[0]
392 + id = strings.TrimSpace(id)
393 + id = strings.ToUpper(id)
394 + return id
395 +}
396 +
397 +func InjectHTML(body []byte) []byte {
398 + // In production, HTML injection is handled by service worker
399 + return body
400 +}
401 +
402 +func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
403 + if strings.HasPrefix(r.URL.Path, "/sw-cgi/websocket/") {
404 + p.handleWebSocketPolyfill(w, r)
405 + return
406 + }
407 +
408 + r = r.Clone(context.Background())
409 +
410 + decodedHost := getLeaseID(r.URL.Hostname())
411 + r.URL.Host = decodedHost
412 + r.URL.Scheme = "http"
413 +
414 + resp, err := httpClient.Do(r)
415 + if err != nil {
416 + http.Error(w, fmt.Sprintf("Failed to proxy request to %s", r.URL.String()), http.StatusBadGateway)
417 + return
418 + }
419 + defer resp.Body.Close()
420 +
421 + for key, value := range resp.Header {
422 + w.Header()[key] = value
423 + }
424 +
425 + if utils.IsHTMLContentType(resp.Header.Get("Content-Type")) {
426 + w.WriteHeader(resp.StatusCode)
427 + body, err := io.ReadAll(resp.Body)
428 + if err != nil {
429 + return
430 + }
431 + w.Write(body)
432 + return
433 + }
434 +
435 + w.WriteHeader(resp.StatusCode)
436 + io.Copy(w, resp.Body)
437 +}
438 +
439 +func (p *Proxy) handleWebSocketPolyfill(w http.ResponseWriter, r *http.Request) {
440 + path := r.URL.Path
441 +
442 + if path == "/sw-cgi/websocket/connect" && r.Method == http.MethodPost {
443 + p.handleConnect(w, r)
444 + return
445 + }
446 +
447 + if strings.HasPrefix(path, "/sw-cgi/websocket/poll/") && r.Method == http.MethodGet {
448 + connID := strings.TrimPrefix(path, "/sw-cgi/websocket/poll/")
449 + p.handlePoll(w, r, connID)
450 + return
451 + }
452 +
453 + if strings.HasPrefix(path, "/sw-cgi/websocket/send/") && r.Method == http.MethodPost {
454 + connID := strings.TrimPrefix(path, "/sw-cgi/websocket/send/")
455 + p.handleSend(w, r, connID)
456 + return
457 + }
458 +
459 + if strings.HasPrefix(path, "/sw-cgi/websocket/disconnect/") && r.Method == http.MethodPost {
460 + connID := strings.TrimPrefix(path, "/sw-cgi/websocket/disconnect/")
461 + p.handleDisconnect(w, r, connID)
462 + return
463 + }
464 +
465 + http.Error(w, "Not found", http.StatusNotFound)
466 +}
467 +
468 +func (p *Proxy) handleConnect(w http.ResponseWriter, r *http.Request) {
469 + var req ConnectRequest
470 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
471 + http.Error(w, "Invalid request", http.StatusBadRequest)
472 + return
473 + }
474 +
475 + wsConn, protocol, err := p.wsManager.CreateConnection(req.URL, req.Protocols)
476 + if err != nil {
477 + http.Error(w, fmt.Sprintf("Failed to connect: %v", err), http.StatusBadGateway)
478 + return
479 + }
480 +
481 + resp := ConnectResponse{
482 + ConnID: wsConn.id,
483 + Protocol: protocol,
484 + }
485 +
486 + w.Header().Set("Content-Type", "application/json")
487 + json.NewEncoder(w).Encode(resp)
488 +}
489 +
490 +func (p *Proxy) handlePoll(w http.ResponseWriter, r *http.Request, connID string) {
491 + wsConn, ok := p.wsManager.GetConnection(connID)
492 + if !ok {
493 + http.Error(w, "Connection not found", http.StatusNotFound)
494 + return
495 + }
496 +
497 + timeout := time.NewTimer(5 * time.Second)
498 + defer timeout.Stop()
499 +
500 + ticker := time.NewTicker(50 * time.Millisecond)
501 + defer ticker.Stop()
502 +
503 + var messages []StreamMessage
504 +
505 + for {
506 + select {
507 + case <-timeout.C:
508 + messages = wsConn.GetMessages()
509 + goto respond
510 +
511 + case <-ticker.C:
512 + messages = wsConn.GetMessages()
513 + if len(messages) > 0 {
514 + goto respond
515 + }
516 +
517 + case <-r.Context().Done():
518 + return
519 + }
520 + }
521 +
522 +respond:
523 + if wsConn.IsClosed() && len(messages) > 0 {
524 + for _, msg := range messages {
525 + if msg.Type == "close" {
526 + defer func() {
527 + p.wsManager.RemoveConnection(connID)
528 + wsConn.Close()
529 + }()
530 + break
531 + }
532 + }
533 + }
534 +
535 + w.Header().Set("Content-Type", "application/json")
536 + w.WriteHeader(http.StatusOK)
537 + json.NewEncoder(w).Encode(map[string]interface{}{
538 + "messages": messages,
539 + })
540 +}
541 +
542 +func (p *Proxy) handleSend(w http.ResponseWriter, r *http.Request, connID string) {
543 + wsConn, ok := p.wsManager.GetConnection(connID)
544 + if !ok {
545 + http.Error(w, "Connection not found", http.StatusNotFound)
546 + return
547 + }
548 +
549 + var req SendRequest
550 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
551 + http.Error(w, "Invalid request", http.StatusBadRequest)
552 + return
553 + }
554 +
555 + if req.Type == "close" {
556 + wsConn.Close()
557 + p.wsManager.RemoveConnection(connID)
558 + w.WriteHeader(http.StatusOK)
559 + return
560 + }
561 +
562 + var data []byte
563 + var err error
564 + var isText bool
565 +
566 + switch req.Type {
567 + case "binary":
568 + data, err = base64.StdEncoding.DecodeString(req.Data)
569 + if err != nil {
570 + http.Error(w, "Invalid base64 data", http.StatusBadRequest)
571 + return
572 + }
573 + isText = false
574 + case "text":
575 + data = []byte(req.Data)
576 + isText = true
577 + default:
578 + http.Error(w, "Invalid message type", http.StatusBadRequest)
579 + return
580 + }
581 +
582 + if err := wsConn.Send(data, isText); err != nil {
583 + http.Error(w, fmt.Sprintf("Failed to send: %v", err), http.StatusInternalServerError)
584 + return
585 + }
586 +
587 + w.WriteHeader(http.StatusOK)
588 +}
589 +
590 +func (p *Proxy) handleDisconnect(w http.ResponseWriter, r *http.Request, connID string) {
591 + wsConn, ok := p.wsManager.GetConnection(connID)
592 + if !ok {
593 + w.WriteHeader(http.StatusOK)
594 + return
595 + }
596 +
597 + wsConn.Close()
598 + p.wsManager.RemoveConnection(connID)
599 +
600 + w.WriteHeader(http.StatusOK)
601 +}
602 +
603 +func handleSDKConnect(data js.Value) {
604 + defer func() {
605 + if r := recover(); r != nil {
606 + }
607 + }()
608 +
609 + if data.Get("leaseName").Type() == js.TypeUndefined || data.Get("clientId").Type() == js.TypeUndefined {
610 + return
611 + }
612 +
613 + leaseName := data.Get("leaseName").String()
614 + clientId := data.Get("clientId").String()
615 +
616 + var upgradeRequest []byte
617 + upgradeReqJS := data.Get("upgradeRequest")
618 + if upgradeReqJS.Type() != js.TypeUndefined && upgradeReqJS.Type() != js.TypeNull {
619 + if upgradeReqJS.InstanceOf(js.Global().Get("Uint8Array")) {
620 + length := upgradeReqJS.Get("length").Int()
621 + upgradeRequest = make([]byte, length)
622 + js.CopyBytesToGo(upgradeRequest, upgradeReqJS)
623 + }
624 + }
625 +
626 + go func() {
627 + defer func() {
628 + if r := recover(); r != nil {
629 + }
630 + }()
631 +
632 + lease, err := client.LookupName(leaseName)
633 + if err != nil {
634 + js.Global().Call("__sdk_post_message", map[string]interface{}{
635 + "type": "SDK_CONNECT_ERROR",
636 + "clientId": clientId,
637 + "error": "lease not found",
638 + })
639 + return
640 + }
641 +
642 + connID := lease.Identity.Id
643 +
644 + cred := sdk.NewCredential()
645 + conn, err := client.Dial(cred, connID, "rdsec/1.0")
646 + if err != nil {
647 + js.Global().Call("__sdk_post_message", map[string]interface{}{
648 + "type": "SDK_CONNECT_ERROR",
649 + "clientId": clientId,
650 + "error": err.Error(),
651 + })
652 + return
653 + }
654 +
655 + sdkConnectionsMu.Lock()
656 + sdkConnections[connID] = conn
657 + sdkConnectionsMu.Unlock()
658 +
659 + js.Global().Call("__sdk_post_message", map[string]interface{}{
660 + "type": "SDK_CONNECT_SUCCESS",
661 + "clientId": clientId,
662 + "connId": connID,
663 + })
664 +
665 + if len(upgradeRequest) > 0 {
666 + if !isValidUpgradeRequest(upgradeRequest) {
667 + js.Global().Call("__sdk_post_message", map[string]interface{}{
668 + "type": "SDK_CONNECT_ERROR",
669 + "clientId": clientId,
670 + "error": "invalid upgrade request",
671 + })
672 + return
673 + }
674 +
675 + _, err = conn.Write(upgradeRequest)
676 + if err != nil {
677 + js.Global().Call("__sdk_post_message", map[string]interface{}{
678 + "type": "SDK_SEND_ERROR",
679 + "clientId": clientId,
680 + "connId": connID,
681 + "error": err.Error(),
682 + })
683 + return
684 + }
685 + }
686 +
687 + buffer := make([]byte, 32*1024)
688 + for {
689 + n, err := conn.Read(buffer)
690 + if err != nil {
691 + sdkConnectionsMu.Lock()
692 + delete(sdkConnections, connID)
693 + sdkConnectionsMu.Unlock()
694 +
695 + js.Global().Call("__sdk_post_message", map[string]interface{}{
696 + "type": "SDK_DATA_CLOSE",
697 + "clientId": clientId,
698 + "connId": connID,
699 + "code": 1000,
700 + })
701 + return
702 + }
703 +
704 + if n > 0 {
705 + data := make([]byte, n)
706 + copy(data, buffer[:n])
707 +
708 + uint8Array := js.Global().Get("Uint8Array").New(n)
709 + js.CopyBytesToJS(uint8Array, data)
710 +
711 + js.Global().Call("__sdk_post_message", map[string]interface{}{
712 + "type": "SDK_DATA",
713 + "clientId": clientId,
714 + "connId": connID,
715 + "data": uint8Array,
716 + })
717 + }
718 + }
719 + }()
720 +}
721 +
722 +func handleSDKSend(data js.Value) {
723 + defer func() {
724 + if r := recover(); r != nil {
725 + }
726 + }()
727 +
728 + if data.Get("connId").Type() == js.TypeUndefined || data.Get("clientId").Type() == js.TypeUndefined || data.Get("data").Type() == js.TypeUndefined {
729 + return
730 + }
731 +
732 + connID := data.Get("connId").String()
733 + clientId := data.Get("clientId").String()
734 + payload := data.Get("data")
735 +
736 + sdkConnectionsMu.RLock()
737 + conn, ok := sdkConnections[connID]
738 + sdkConnectionsMu.RUnlock()
739 +
740 + if !ok {
741 + js.Global().Call("__sdk_post_message", map[string]interface{}{
742 + "type": "SDK_SEND_ERROR",
743 + "clientId": clientId,
744 + "connId": connID,
745 + "error": "connection not found",
746 + })
747 + return
748 + }
749 +
750 + var bytes []byte
751 + if payload.InstanceOf(js.Global().Get("Uint8Array")) {
752 + length := payload.Get("length").Int()
753 + bytes = make([]byte, length)
754 + js.CopyBytesToGo(bytes, payload)
755 + } else if payload.InstanceOf(js.Global().Get("ArrayBuffer")) {
756 + uint8Array := js.Global().Get("Uint8Array").New(payload)
757 + length := uint8Array.Get("length").Int()
758 + bytes = make([]byte, length)
759 + js.CopyBytesToGo(bytes, uint8Array)
760 + } else {
761 + return
762 + }
763 +
764 + go func() {
765 + _, err := conn.Write(bytes)
766 + if err != nil {
767 + js.Global().Call("__sdk_post_message", map[string]interface{}{
768 + "type": "SDK_SEND_ERROR",
769 + "clientId": clientId,
770 + "connId": connID,
771 + "error": err.Error(),
772 + })
773 + }
774 + }()
775 +}
776 +
777 +func handleSDKClose(data js.Value) {
778 + defer func() {
779 + if r := recover(); r != nil {
780 + }
781 + }()
782 +
783 + if data.Get("connId").Type() == js.TypeUndefined || data.Get("clientId").Type() == js.TypeUndefined {
784 + return
785 + }
786 +
787 + connID := data.Get("connId").String()
788 + clientId := data.Get("clientId").String()
789 +
790 + sdkConnectionsMu.Lock()
791 + conn, ok := sdkConnections[connID]
792 + if ok {
793 + delete(sdkConnections, connID)
794 + }
795 + sdkConnectionsMu.Unlock()
796 +
797 + if !ok {
798 + return
799 + }
800 +
801 + conn.Close()
802 +
803 + js.Global().Call("__sdk_post_message", map[string]interface{}{
804 + "type": "SDK_DATA_CLOSE",
805 + "clientId": clientId,
806 + "connId": connID,
807 + "code": 1000,
808 + })
809 +}
810 +
811 +func main() {
812 + bootstrapServerList := getBootstrapServers()
813 +
814 + var err error
815 + client, err = sdk.NewClient(
816 + sdk.WithBootstrapServers(bootstrapServerList),
817 + sdk.WithDialer(WebSocketDialerJS()),
818 + )
819 + if err != nil {
820 + panic(err)
821 + }
822 + defer client.Close()
823 +
824 + dialerCredential = sdk.NewCredential()
825 +
826 + wsManager := NewWebSocketManager()
827 + proxy := &Proxy{
828 + wsManager: wsManager,
829 + }
830 +
831 + js.Global().Set("__go_jshttp", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
832 + if len(args) < 1 {
833 + return js.Global().Get("Promise").Call("reject",
834 + js.Global().Get("Error").New("required parameter JSRequest missing"))
835 + }
836 +
837 + jsReq := args[0]
838 + return httpjs.ServeHTTPAsyncWithStreaming(proxy, jsReq)
839 + }))
840 +
841 + js.Global().Set("__sdk_message_handler", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
842 + if len(args) < 2 {
843 + return nil
844 + }
845 +
846 + messageType := args[0].String()
847 + data := args[1]
848 +
849 + switch messageType {
850 + case "SDK_CONNECT":
851 + handleSDKConnect(data)
852 + case "SDK_SEND":
853 + handleSDKSend(data)
854 + case "SDK_CLOSE":
855 + handleSDKClose(data)
856 + }
857 +
858 + return nil
859 + }))
860 +
861 + if runtime.Compiler == "tinygo" {
862 + return
863 + }
864 +
865 + ch := make(chan bool)
866 + <-ch
867 +}