@@ -43,6 +43,10 @@ export class WalletLinkConnection {
4343 private nextReqId = IntNumber ( 1 ) ;
4444 private heartbeatIntervalId ?: number ;
4545 private reconnectAttempts = 0 ;
46+ private visibilityChangeHandler ?: ( ) => void ;
47+ private focusHandler ?: ( ) => void ;
48+ private activeWsInstance ?: WalletLinkWebSocket ;
49+ private isReconnecting = false ;
4650
4751 private readonly session : Session ;
4852
@@ -51,6 +55,8 @@ export class WalletLinkConnection {
5155 private cipher : Cipher ;
5256 private ws : WalletLinkWebSocket ;
5357 private http : WalletLinkHTTP ;
58+ private readonly linkAPIUrl : string ;
59+ private readonly WebSocketClass : typeof WebSocket ;
5460
5561 /**
5662 * Constructor
@@ -70,16 +76,38 @@ export class WalletLinkConnection {
7076 this . cipher = new Cipher ( session . secret ) ;
7177 this . diagnostic = diagnostic ;
7278 this . listener = listener ;
79+ this . linkAPIUrl = linkAPIUrl ;
80+ this . WebSocketClass = WebSocketClass ;
7381
7482 console . debug ( '[WalletLinkConnection] Creating new WalletLinkWebSocket instance' ) ;
75- const ws = new WalletLinkWebSocket ( `${ linkAPIUrl } /rpc` , WebSocketClass ) ;
83+ const ws = this . createWebSocket ( ) ;
84+ this . ws = ws ;
85+
86+ this . http = new WalletLinkHTTP ( linkAPIUrl , session . id , session . key ) ;
87+
88+ // Set up visibility and focus handlers for mobile Safari
89+ this . setupMobileSafariHandlers ( ) ;
90+ }
91+
92+ private createWebSocket ( ) : WalletLinkWebSocket {
93+ const ws = new WalletLinkWebSocket ( `${ this . linkAPIUrl } /rpc` , this . WebSocketClass ) ;
7694 console . debug ( '[WalletLinkConnection] WebSocket instance created. Total active WebSocket instances:' , ( WalletLinkWebSocket as any ) . getActiveInstances ( ) ) ;
95+
96+ // Track this as the active WebSocket instance
97+ this . activeWsInstance = ws ;
98+
7799 ws . setConnectionStateListener ( async ( state ) => {
100+ // Ignore events from non-active WebSocket instances
101+ if ( ws !== this . activeWsInstance ) {
102+ console . debug ( '[WalletLinkConnection] Ignoring state change from non-active WebSocket instance' ) ;
103+ return ;
104+ }
105+
78106 // attempt to reconnect every 5 seconds when disconnected
79107 console . debug ( '[WalletLinkConnection] Connection state changed to:' , ConnectionState [ state ] , 'shouldFetchUnseenEventsOnConnect:' , this . shouldFetchUnseenEventsOnConnect ) ;
80108 this . diagnostic ?. log ( EVENTS . CONNECTED_STATE_CHANGE , {
81109 state,
82- sessionIdHash : Session . hash ( session . id ) ,
110+ sessionIdHash : Session . hash ( this . session . id ) ,
83111 } ) ;
84112
85113 let connected = false ;
@@ -92,26 +120,55 @@ export class WalletLinkConnection {
92120 console . debug ( '[WalletLinkConnection] Cleared heartbeat timer' ) ;
93121 }
94122
123+ // Reset lastHeartbeatResponse to prevent false timeout on reconnection
124+ this . lastHeartbeatResponse = 0 ;
125+ console . debug ( '[WalletLinkConnection] Reset lastHeartbeatResponse on disconnect' ) ;
126+
95127 // Reset connected state to false on disconnect
96128 connected = false ;
97129 console . debug ( '[WalletLinkConnection] DISCONNECTED case - shouldFetchUnseenEventsOnConnect:' , this . shouldFetchUnseenEventsOnConnect ) ;
98130
99- // if DISCONNECTED and not destroyed
131+ // if DISCONNECTED and not destroyed, create a fresh WebSocket connection
100132 if ( ! this . destroyed ) {
101- const connect = async ( ) => {
133+ const reconnect = async ( ) => {
134+ // Prevent multiple concurrent reconnection attempts
135+ if ( this . isReconnecting ) {
136+ console . debug ( '[WalletLinkConnection] Reconnection already in progress, skipping' ) ;
137+ return ;
138+ }
139+
140+ this . isReconnecting = true ;
141+
142+ // Calculate delay with exponential backoff (max 30 seconds)
143+ const delay = Math . min ( 1000 * Math . pow ( 2 , this . reconnectAttempts ) , 30000 ) ;
144+ console . debug ( `[WalletLinkConnection] Waiting ${ delay } ms before reconnection attempt ${ this . reconnectAttempts + 1 } ` ) ;
145+
102146 // wait with exponential backoff
103147 await new Promise ( ( resolve ) => setTimeout ( resolve , delay ) ) ;
104- // check whether it's destroyed again
105- if ( ! this . destroyed && state === ConnectionState . DISCONNECTED ) {
106- // reconnect
148+
149+ // check whether it's destroyed again and ensure this is still the active instance
150+ if ( ! this . destroyed && ws === this . activeWsInstance ) {
107151 this . reconnectAttempts ++ ;
108- console . debug ( '[WalletLinkConnection] Attempting to reconnect' ) ;
109- ws . connect ( ) . catch ( ( ) => {
110- connect ( ) ;
152+ console . debug ( '[WalletLinkConnection] Creating fresh WebSocket for reconnection' ) ;
153+
154+ // Clean up the old WebSocket instance
155+ if ( 'cleanup' in this . ws && typeof this . ws . cleanup === 'function' ) {
156+ ( this . ws as any ) . cleanup ( ) ;
157+ }
158+
159+ // Create a fresh WebSocket instance
160+ this . ws = this . createWebSocket ( ) ;
161+ this . ws . connect ( ) . catch ( ( ) => {
162+ console . error ( '[WalletLinkConnection] Reconnection failed, will retry' ) ;
163+ } ) . finally ( ( ) => {
164+ this . isReconnecting = false ;
111165 } ) ;
166+ } else {
167+ console . debug ( '[WalletLinkConnection] Skipping reconnection - destroyed or not active instance' ) ;
168+ this . isReconnecting = false ;
112169 }
113170 } ;
114- connect ( ) ;
171+ reconnect ( ) ;
115172 }
116173 break ;
117174
@@ -144,8 +201,15 @@ export class WalletLinkConnection {
144201 } ) ;
145202 } catch ( error ) {
146203 console . error ( '[WalletLinkConnection] Authentication failed:' , error ) ;
204+ // Don't set connected to true if authentication fails
205+ break ;
147206 }
148207
208+ // Update connected state immediately after successful authentication
209+ // This ensures heartbeats won't be skipped
210+ console . debug ( '[WalletLinkConnection] Setting connected state to true before starting heartbeat' ) ;
211+ this . connected = connected ;
212+
149213 // send heartbeat every n seconds while connected
150214 // if CONNECTED, start the heartbeat timer
151215 // first timer event updates lastHeartbeat timestamp
@@ -162,17 +226,28 @@ export class WalletLinkConnection {
162226 } , HEARTBEAT_INTERVAL ) ;
163227 console . debug ( '[WalletLinkConnection] Started heartbeat timer' ) ;
164228
229+ // Send an immediate heartbeat to verify connection is alive
230+ // This is especially important for reconnections
231+ setTimeout ( ( ) => {
232+ console . debug ( '[WalletLinkConnection] Sending initial heartbeat after connection' ) ;
233+ this . heartbeat ( ) ;
234+ } , 100 ) ;
235+
165236 break ;
166237
167238 case ConnectionState . CONNECTING :
168239 console . debug ( '[WalletLinkConnection] Connection in progress' ) ;
169240 break ;
170241 }
171242
172- // Always update connected state to ensure proper transitions
173- console . debug ( '[WalletLinkConnection] Updating connected state from' , this . connected , 'to' , connected ) ;
174- this . connected = connected ;
243+ // Update connected state for DISCONNECTED and CONNECTING cases
244+ // For CONNECTED case, it's already set above
245+ if ( state !== ConnectionState . CONNECTED ) {
246+ console . debug ( '[WalletLinkConnection] Updating connected state from' , this . connected , 'to' , connected ) ;
247+ this . connected = connected ;
248+ }
175249 } ) ;
250+
176251 ws . setIncomingDataListener ( ( m ) => {
177252 console . debug ( '[WalletLinkConnection] Received message type:' , m . type , 'Full message:' , m ) ;
178253 switch ( m . type ) {
@@ -187,7 +262,7 @@ export class WalletLinkConnection {
187262 const linked = m . type === 'IsLinkedOK' ? m . linked : undefined ;
188263 console . debug ( '[WalletLinkConnection] Link status update:' , { linked, onlineGuests : m . onlineGuests } ) ;
189264 this . diagnostic ?. log ( EVENTS . LINKED , {
190- sessionIdHash : Session . hash ( session . id ) ,
265+ sessionIdHash : Session . hash ( this . session . id ) ,
191266 linked,
192267 type : m . type ,
193268 onlineGuests : m . onlineGuests ,
@@ -202,7 +277,7 @@ export class WalletLinkConnection {
202277 case 'SessionConfigUpdated' : {
203278 console . debug ( '[WalletLinkConnection] Session config received:' , m . metadata ) ;
204279 this . diagnostic ?. log ( EVENTS . SESSION_CONFIG_RECEIVED , {
205- sessionIdHash : Session . hash ( session . id ) ,
280+ sessionIdHash : Session . hash ( this . session . id ) ,
206281 metadata_keys : m && m . metadata ? Object . keys ( m . metadata ) : undefined ,
207282 } ) ;
208283 this . handleSessionMetadataUpdated ( m . metadata ) ;
@@ -233,9 +308,77 @@ export class WalletLinkConnection {
233308 this . requestResolutions . get ( m . id ) ?.( m ) ;
234309 }
235310 } ) ;
236- this . ws = ws ;
311+
312+ return ws ;
313+ }
237314
238- this . http = new WalletLinkHTTP ( linkAPIUrl , session . id , session . key ) ;
315+ private setupMobileSafariHandlers ( ) : void {
316+ // Handle visibility changes (when app is backgrounded/foregrounded)
317+ this . visibilityChangeHandler = ( ) => {
318+ console . debug ( '[WalletLinkConnection] Visibility changed, document.hidden:' , document . hidden ) ;
319+
320+ if ( ! document . hidden && ! this . destroyed ) {
321+ // Page became visible - check connection and reconnect if needed
322+ console . debug ( '[WalletLinkConnection] Page became visible, checking connection status' ) ;
323+
324+ // Force a fresh connection if we're disconnected
325+ if ( ! this . connected ) {
326+ console . debug ( '[WalletLinkConnection] Not connected, forcing fresh connection' ) ;
327+ this . reconnectWithFreshWebSocket ( ) ;
328+ } else {
329+ // Send a heartbeat to check if connection is still alive
330+ this . heartbeat ( ) ;
331+ }
332+ }
333+ } ;
334+
335+ // Handle focus events (when user switches back to the tab/app)
336+ this . focusHandler = ( ) => {
337+ console . debug ( '[WalletLinkConnection] Window focused' ) ;
338+
339+ if ( ! this . destroyed && ! this . connected ) {
340+ console . debug ( '[WalletLinkConnection] Window focused but not connected, forcing fresh connection' ) ;
341+ this . reconnectWithFreshWebSocket ( ) ;
342+ }
343+ } ;
344+
345+ // Add event listeners
346+ document . addEventListener ( 'visibilitychange' , this . visibilityChangeHandler ) ;
347+ window . addEventListener ( 'focus' , this . focusHandler ) ;
348+
349+ // Also handle pageshow event for iOS Safari
350+ window . addEventListener ( 'pageshow' , ( event ) => {
351+ if ( event . persisted ) {
352+ console . debug ( '[WalletLinkConnection] Page restored from bfcache' ) ;
353+ if ( this . focusHandler ) {
354+ this . focusHandler ( ) ;
355+ }
356+ }
357+ } ) ;
358+ }
359+
360+ private reconnectWithFreshWebSocket ( ) : void {
361+ if ( this . destroyed ) return ;
362+
363+ console . debug ( '[WalletLinkConnection] Reconnecting with fresh WebSocket' ) ;
364+
365+ // Clear the active instance reference before disconnecting
366+ const oldWs = this . ws ;
367+ this . activeWsInstance = undefined ;
368+
369+ // Disconnect current WebSocket
370+ oldWs . disconnect ( ) ;
371+
372+ // Clean up the old instance
373+ if ( 'cleanup' in oldWs && typeof oldWs . cleanup === 'function' ) {
374+ ( oldWs as any ) . cleanup ( ) ;
375+ }
376+
377+ // Create and connect fresh WebSocket
378+ this . ws = this . createWebSocket ( ) ;
379+ this . ws . connect ( ) . catch ( ( error ) => {
380+ console . error ( '[WalletLinkConnection] Fresh reconnection failed:' , error ) ;
381+ } ) ;
239382 }
240383
241384 /**
@@ -257,6 +400,9 @@ export class WalletLinkConnection {
257400 */
258401 public destroy ( ) : void {
259402 this . destroyed = true ;
403+
404+ // Clear the active instance reference
405+ this . activeWsInstance = undefined ;
260406
261407 // Clear heartbeat timer
262408 if ( this . heartbeatIntervalId ) {
@@ -265,6 +411,14 @@ export class WalletLinkConnection {
265411 console . debug ( '[WalletLinkConnection] Cleared heartbeat timer on destroy' ) ;
266412 }
267413
414+ // Remove event listeners
415+ if ( this . visibilityChangeHandler ) {
416+ document . removeEventListener ( 'visibilitychange' , this . visibilityChangeHandler ) ;
417+ }
418+ if ( this . focusHandler ) {
419+ window . removeEventListener ( 'focus' , this . focusHandler ) ;
420+ }
421+
268422 console . debug ( '[WalletLinkConnection] Destroying connection - calling disconnect' ) ;
269423 this . ws . disconnect ( ) ;
270424
0 commit comments