feat: implement long polling for WebSocket proxy

- Updated handlePoll in main_js.go to wait up to 5 seconds for messages, checking every 50ms, instead of immediate retrieval - Refactored _startPolling in polyfill.js from interval-based polling every 100ms to continuous long polling - Improves efficiency by reducing server requests and enhancing responsiveness for message delivery

lemon-mint committed Nov 1, 2025 at 02:24 UTC c5732f8ee4853da3f6d4859b1c244ae934fe2466
2 files changed +38 -24
cmd/webclient/main_js.go
+29 -2
@@ -400,9 +400,36 @@ func (p *Proxy) handlePoll(w http.ResponseWriter, r *http.Request, connID string
400 return
401 }
402
403 - // Get queued messages
404 - messages := wsConn.GetMessages()
403 + // Long polling: wait up to 5 seconds for messages
404 + timeout := time.NewTimer(5 * time.Second)
405 + defer timeout.Stop()
406
407 + ticker := time.NewTicker(50 * time.Millisecond)
408 + defer ticker.Stop()
409 +
410 + var messages []StreamMessage
411 +
412 + for {
413 + select {
414 + case <-timeout.C:
415 + // Timeout - return empty or existing messages
416 + messages = wsConn.GetMessages()
417 + goto respond
418 +
419 + case <-ticker.C:
420 + // Check for messages periodically
421 + messages = wsConn.GetMessages()
422 + if len(messages) > 0 {
423 + goto respond
424 + }
425 +
426 + case <-r.Context().Done():
427 + // Client disconnected
428 + return
429 + }
430 + }
431 +
432 +respond:
433 // Check if connection is closed and cleanup if needed
434 if wsConn.IsClosed() && len(messages) > 0 {
435 // Check if close message is in the queue
cmd/webclient/polyfill.js
+9 -22
@@ -71,16 +71,10 @@
71 }
72 }
73
74 - _startPolling() {
75 - if (!this._connId || this._isClosed) return;
76 -
77 - // Poll every 100ms
78 - this._pollInterval = setInterval(async () => {
79 - if (this._isClosed) {
80 - clearInterval(this._pollInterval);
81 - return;
82 - }
83 -
74 + async _startPolling() {
75 + // Long polling: continuously fetch messages
76 + // Server will wait up to 5 seconds before responding
77 + while (!this._isClosed) {
78 try {
79 const response = await fetch(`/sw-cgi/websocket/poll/${this._connId}`, {
80 method: 'GET'
@@ -99,16 +93,12 @@
93 }
94
95 } catch (error) {
102 - console.error('Polling error:', error);
103 - this._handleError(error);
96 + if (!this._isClosed) {
97 + console.error('Polling error:', error);
98 + this._handleError(error);
99 + }
100 + break;
101 }
105 - }, 100);
106 - }
107 -
108 - _stopPolling() {
109 - if (this._pollInterval) {
110 - clearInterval(this._pollInterval);
111 - this._pollInterval = null;
102 }
103 }
104
@@ -180,9 +170,6 @@
170 this._isClosed = true;
171 this.readyState = WebSocket.CLOSED;
172
183 - // Stop polling
184 - this._stopPolling();
185 -
173 // Create CloseEvent
174 const event = new CloseEvent('close', {
175 code: code,