@@ -44,6 +44,67 @@ Genie.initWebChannel = function(channel = Genie.Settings.webchannels_default_rou
4444 }
4545
4646 var WebChannel = { } ;
47+ WebChannel . _isUnloading = false ;
48+ WebChannel . _reconnectAttempts = 0 ;
49+ WebChannel . _reconnectMinDelay = Genie . Settings . webchannels_reconnect_delay ;
50+ WebChannel . _reconnectMaxDelay = Math . max ( 30000 , Genie . Settings . webchannels_reconnect_delay * 16 ) ;
51+
52+ const logDev = ( ...args ) => {
53+ if ( isDev ( ) ) console . info ( '[Genie.WebChannels]' , ...args ) ;
54+ } ;
55+
56+ const warnDev = ( ...args ) => {
57+ if ( isDev ( ) ) console . warn ( '[Genie.WebChannels]' , ...args ) ;
58+ } ;
59+
60+ const nextReconnectDelay = ( ) => {
61+ const expDelay = Math . min (
62+ WebChannel . _reconnectMaxDelay ,
63+ WebChannel . _reconnectMinDelay * Math . pow ( 2 , WebChannel . _reconnectAttempts )
64+ ) ;
65+ // Add jitter so multiple clients do not reconnect in lockstep.
66+ const jitter = Math . floor ( expDelay * 0.25 * Math . random ( ) ) ;
67+ return expDelay + jitter ;
68+ } ;
69+
70+ const scheduleReconnect = ( reason = 'close' ) => {
71+ if ( WebChannel . _isUnloading || ! Genie . Settings . webchannels_autosubscribe ) {
72+ return ;
73+ }
74+
75+ clearTimeout ( WebChannel . _reconnectTimer ) ;
76+ const delay = nextReconnectDelay ( ) ;
77+ warnDev ( 'Scheduling reconnect' , {
78+ channel : WebChannel . channel ,
79+ reason,
80+ attempt : WebChannel . _reconnectAttempts + 1 ,
81+ delay
82+ } ) ;
83+
84+ WebChannel . _reconnectTimer = setTimeout ( function ( ) {
85+ if ( WebChannel . _isUnloading ) return ;
86+ WebChannel . _openConnectionPromise = null ;
87+ WebChannel . _reconnectAttempts ++ ;
88+ logDev ( 'Reconnecting now' , {
89+ channel : WebChannel . channel ,
90+ attempt : WebChannel . _reconnectAttempts
91+ } ) ;
92+ WebChannel . socket = newSocketConnection ( WebChannel ) ;
93+ } , delay ) ;
94+ } ;
95+
96+ const triggerImmediateReconnect = ( reason = 'manual' ) => {
97+ if ( WebChannel . _isUnloading || ! Genie . Settings . webchannels_autosubscribe ) {
98+ return ;
99+ }
100+ if ( ! WebChannel . socket || WebChannel . socket . readyState > 1 ) {
101+ warnDev ( 'Immediate reconnect trigger' , { channel : WebChannel . channel , reason } ) ;
102+ clearTimeout ( WebChannel . _reconnectTimer ) ;
103+ WebChannel . _openConnectionPromise = null ;
104+ WebChannel . socket = newSocketConnection ( WebChannel ) ;
105+ }
106+ } ;
107+
47108 WebChannel . sendMessageTo = async ( channel , message , payload = { } ) => {
48109 let msg = JSON . stringify ( {
49110 'channel' : channel ,
@@ -140,33 +201,55 @@ Genie.initWebChannel = function(channel = Genie.Settings.webchannels_default_rou
140201 } ) ;
141202
142203 WebChannel . closeHandlers . push ( event => {
143- if ( isDev ( ) ) {
144- console . warn ( 'WebSocket connection closed: ' + event . code + ' ' + event . reason + ' ' + event . wasClean ) ;
145- }
204+ warnDev ( 'WebSocket closed' , {
205+ channel : WebChannel . channel ,
206+ code : event . code ,
207+ reason : event . reason ,
208+ wasClean : event . wasClean
209+ } ) ;
146210 } ) ;
147211
148212 WebChannel . closeHandlers . push ( event => {
149213 displayAlert ( WebChannel ) ;
150- if ( Genie . Settings . webchannels_autosubscribe ) {
151- if ( isDev ( ) ) console . info ( 'Attempting to reconnect! ' ) ;
152- clearTimeout ( WebChannel . _reconnectTimer ) ;
153- WebChannel . _reconnectTimer = setTimeout ( function ( ) {
154- WebChannel . _openConnectionPromise = null ;
155- WebChannel . socket = newSocketConnection ( WebChannel ) ;
156- } , Genie . Settings . webchannels_reconnect_delay ) ;
157- }
214+ scheduleReconnect ( 'socket-close' ) ;
158215 } ) ;
159216
160217 WebChannel . openHandlers . push ( event => {
218+ WebChannel . _reconnectAttempts = 0 ;
219+ clearTimeout ( WebChannel . _reconnectTimer ) ;
220+ logDev ( 'WebSocket opened' , { channel : WebChannel . channel } ) ;
161221 if ( Genie . Settings . webchannels_autosubscribe ) {
162222 subscribe ( WebChannel ) ;
163223 }
164224 } ) ;
225+
226+ WebChannel . errorHandlers . push ( event => {
227+ warnDev ( 'WebSocket error observed' , {
228+ channel : WebChannel . channel ,
229+ readyState : WebChannel . socket ?. readyState ,
230+ type : event . type
231+ } ) ;
232+ } ) ;
233+
234+ WebChannel . _onWindowFocus = _ => triggerImmediateReconnect ( 'window-focus' ) ;
235+ WebChannel . _onWindowOnline = _ => triggerImmediateReconnect ( 'browser-online' ) ;
236+ WebChannel . _onVisibilityChange = _ => {
237+ if ( document . visibilityState === 'visible' ) {
238+ triggerImmediateReconnect ( 'visibility-visible' ) ;
239+ }
240+ } ;
241+ window . addEventListener ( 'focus' , WebChannel . _onWindowFocus ) ;
242+ window . addEventListener ( 'online' , WebChannel . _onWindowOnline ) ;
243+ document . addEventListener ( 'visibilitychange' , WebChannel . _onVisibilityChange ) ;
165244
166245 window . addEventListener ( 'beforeunload' , _ => {
167- if ( isDev ( ) ) {
168- console . info ( 'Preparing to unload' ) ;
169- }
246+ WebChannel . _isUnloading = true ;
247+ logDev ( 'Preparing to unload' , { channel : WebChannel . channel } ) ;
248+ clearTimeout ( WebChannel . _reconnectTimer ) ;
249+
250+ if ( WebChannel . _onWindowFocus ) window . removeEventListener ( 'focus' , WebChannel . _onWindowFocus ) ;
251+ if ( WebChannel . _onWindowOnline ) window . removeEventListener ( 'online' , WebChannel . _onWindowOnline ) ;
252+ if ( WebChannel . _onVisibilityChange ) document . removeEventListener ( 'visibilitychange' , WebChannel . _onVisibilityChange ) ;
170253
171254 if ( Genie . Settings . webchannels_autosubscribe ) {
172255 unsubscribe ( WebChannel ) ;
@@ -401,12 +484,14 @@ function subscription_ready(WebChannel) {
401484
402485function subscribe ( WebChannel , trial = 1 ) {
403486 if ( WebChannel . socket . readyState == 1 && ( document . readyState === 'complete' || document . readyState === 'interactive' ) ) {
487+ if ( isDev ( ) ) console . info ( '[Genie.WebChannels] Sending subscription request' , { channel : WebChannel . channel , trial } ) ;
404488 WebChannel . sendMessageTo ( WebChannel . channel , window . Genie . Settings . webchannels_subscribe_channel ) ;
405489 } else if ( trial < Genie . Settings . webchannels_subscription_trials ) {
406- if ( isDev ( ) ) console . warn ( 'Queuing subscription' ) ;
490+ if ( isDev ( ) ) console . warn ( '[Genie.WebChannels] Queuing subscription' , { channel : WebChannel . channel , trial , state : WebChannel . socket . readyState } ) ;
407491 clearTimeout ( WebChannel . _subscribeTimer ) ;
408492 WebChannel . _subscribeTimer = setTimeout ( subscribe . bind ( this , WebChannel , trial + 1 ) , Genie . Settings . webchannels_timeout ) ;
409493 } else {
494+ if ( isDev ( ) ) console . warn ( '[Genie.WebChannels] Subscription retries exhausted; waiting for reconnect triggers' , { channel : WebChannel . channel , trial } ) ;
410495 displayAlert ( WebChannel ) ;
411496 }
412497} ;
0 commit comments