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
+}