Enhance WebSocket service with polling fallback and improved diagnostics
- Added configuration options for polling fallback and polling interval to the WebSocket service, enabling a backup mechanism for real-time updates when WebSocket connections fail. - Implemented detailed logging for WebSocket events, including connection status, message handling, and error diagnostics, to improve monitoring and debugging capabilities. - Updated connection handling to manage polling effectively, ensuring users receive timely updates even during WebSocket disruptions. These changes enhance the reliability and transparency of the WalletWebSocketService, contributing to a better user experience.
This commit is contained in:
parent
37a07c0c12
commit
ed92c7ba87
2 changed files with 307 additions and 19 deletions
|
|
@ -78,7 +78,9 @@ export const appConfig: AppConfig = {
|
||||||
websocket: {
|
websocket: {
|
||||||
enabled: import.meta.env.VITE_WEBSOCKET_ENABLED !== 'false', // Can be disabled via env var
|
enabled: import.meta.env.VITE_WEBSOCKET_ENABLED !== 'false', // Can be disabled via env var
|
||||||
reconnectDelay: 2000, // 2 seconds (increased from 1s to reduce server load)
|
reconnectDelay: 2000, // 2 seconds (increased from 1s to reduce server load)
|
||||||
maxReconnectAttempts: 3 // Reduced from 5 to avoid overwhelming server
|
maxReconnectAttempts: 3, // Reduced from 5 to avoid overwhelming server
|
||||||
|
fallbackToPolling: true, // Enable polling fallback when WebSocket fails
|
||||||
|
pollingInterval: 10000 // 10 seconds for polling updates
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ interface WebSocketConfig {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
reconnectDelay: number
|
reconnectDelay: number
|
||||||
maxReconnectAttempts: number
|
maxReconnectAttempts: number
|
||||||
|
fallbackToPolling: boolean
|
||||||
|
pollingInterval: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WalletWebSocketService extends BaseService {
|
export class WalletWebSocketService extends BaseService {
|
||||||
|
|
@ -21,10 +23,16 @@ export class WalletWebSocketService extends BaseService {
|
||||||
private ws: WebSocket | null = null
|
private ws: WebSocket | null = null
|
||||||
private reconnectTimer: NodeJS.Timeout | null = null
|
private reconnectTimer: NodeJS.Timeout | null = null
|
||||||
private reconnectAttempts = 0
|
private reconnectAttempts = 0
|
||||||
|
private pollingTimer: NodeJS.Timeout | null = null
|
||||||
|
private stabilityTimer: NodeJS.Timeout | null = null
|
||||||
|
private usingPollingFallback = false
|
||||||
|
private consecutiveFailures = 0
|
||||||
private config: WebSocketConfig = {
|
private config: WebSocketConfig = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
reconnectDelay: 1000,
|
reconnectDelay: 1000,
|
||||||
maxReconnectAttempts: 5
|
maxReconnectAttempts: 5,
|
||||||
|
fallbackToPolling: true,
|
||||||
|
pollingInterval: 10000 // 10 seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service dependencies (auto-injected by BaseService)
|
// Service dependencies (auto-injected by BaseService)
|
||||||
|
|
@ -143,14 +151,22 @@ export class WalletWebSocketService extends BaseService {
|
||||||
|
|
||||||
this.connectionStatus.value = 'connecting'
|
this.connectionStatus.value = 'connecting'
|
||||||
|
|
||||||
// Create WebSocket connection
|
// Create WebSocket connection with detailed monitoring
|
||||||
this.ws = new WebSocket(wsUrl)
|
this.ws = new WebSocket(wsUrl)
|
||||||
|
|
||||||
// Set up event handlers
|
// Log initial WebSocket state
|
||||||
this.ws.onopen = this.handleOpen.bind(this)
|
console.log('WalletWebSocketService: WebSocket created', {
|
||||||
this.ws.onmessage = this.handleMessage.bind(this)
|
readyState: this.ws.readyState,
|
||||||
this.ws.onclose = this.handleClose.bind(this)
|
url: this.ws.url,
|
||||||
this.ws.onerror = this.handleWebSocketError.bind(this)
|
protocol: this.ws.protocol,
|
||||||
|
extensions: this.ws.extensions
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set up event handlers with enhanced diagnostics
|
||||||
|
this.ws.onopen = (event) => this.handleOpenWithDiagnostics(event)
|
||||||
|
this.ws.onmessage = (event) => this.handleMessageWithDiagnostics(event)
|
||||||
|
this.ws.onclose = (event) => this.handleCloseWithDiagnostics(event)
|
||||||
|
this.ws.onerror = (event) => this.handleWebSocketErrorWithDiagnostics(event)
|
||||||
|
|
||||||
console.log('WalletWebSocketService: WebSocket created, waiting for connection...')
|
console.log('WalletWebSocketService: WebSocket created, waiting for connection...')
|
||||||
|
|
||||||
|
|
@ -165,10 +181,12 @@ export class WalletWebSocketService extends BaseService {
|
||||||
* Handle WebSocket connection opened
|
* Handle WebSocket connection opened
|
||||||
*/
|
*/
|
||||||
private handleOpen(_event: Event): void {
|
private handleOpen(_event: Event): void {
|
||||||
|
// Record connection start time for duration tracking
|
||||||
|
this.connectionStartTime = Date.now()
|
||||||
|
|
||||||
console.log('WalletWebSocketService: Connected successfully')
|
console.log('WalletWebSocketService: Connected successfully')
|
||||||
this.isConnected.value = true
|
this.isConnected.value = true
|
||||||
this.connectionStatus.value = 'connected'
|
this.connectionStatus.value = 'connected'
|
||||||
this.reconnectAttempts = 0
|
|
||||||
|
|
||||||
// Clear any pending reconnect timer
|
// Clear any pending reconnect timer
|
||||||
if (this.reconnectTimer) {
|
if (this.reconnectTimer) {
|
||||||
|
|
@ -176,6 +194,26 @@ export class WalletWebSocketService extends BaseService {
|
||||||
this.reconnectTimer = null
|
this.reconnectTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only reset counters after connection has been stable for 10 seconds
|
||||||
|
this.stabilityTimer = setTimeout(() => {
|
||||||
|
if (this.isConnected.value) {
|
||||||
|
console.log('WalletWebSocketService: Connection stable, resetting retry counters')
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
this.consecutiveFailures = 0
|
||||||
|
|
||||||
|
// Stop polling fallback if we have a stable WebSocket connection
|
||||||
|
if (this.usingPollingFallback) {
|
||||||
|
this.stopPollingFallback()
|
||||||
|
if (this.toast) {
|
||||||
|
this.toast.success('WebSocket reconnected', {
|
||||||
|
description: 'Real-time updates restored via WebSocket connection.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.stabilityTimer = null
|
||||||
|
}, 10000)
|
||||||
|
|
||||||
// Send a ping to test connection stability
|
// Send a ping to test connection stability
|
||||||
this.sendPing()
|
this.sendPing()
|
||||||
}
|
}
|
||||||
|
|
@ -285,6 +323,12 @@ export class WalletWebSocketService extends BaseService {
|
||||||
this.connectionStatus.value = 'disconnected'
|
this.connectionStatus.value = 'disconnected'
|
||||||
this.ws = null
|
this.ws = null
|
||||||
|
|
||||||
|
// Clear stability timer since connection closed
|
||||||
|
if (this.stabilityTimer) {
|
||||||
|
clearTimeout(this.stabilityTimer)
|
||||||
|
this.stabilityTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
// Handle specific close codes
|
// Handle specific close codes
|
||||||
if (event.code === 1006) {
|
if (event.code === 1006) {
|
||||||
console.warn('WalletWebSocketService: Abnormal closure detected - possible server issue')
|
console.warn('WalletWebSocketService: Abnormal closure detected - possible server issue')
|
||||||
|
|
@ -321,16 +365,24 @@ export class WalletWebSocketService extends BaseService {
|
||||||
* Schedule a reconnection attempt
|
* Schedule a reconnection attempt
|
||||||
*/
|
*/
|
||||||
private scheduleReconnect(isAbnormalClosure = false): void {
|
private scheduleReconnect(isAbnormalClosure = false): void {
|
||||||
|
this.consecutiveFailures++
|
||||||
|
|
||||||
// Don't reconnect if we've exceeded max attempts
|
// Don't reconnect if we've exceeded max attempts
|
||||||
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
||||||
console.log('WalletWebSocketService: Max reconnection attempts reached - disabling WebSocket')
|
console.log('WalletWebSocketService: Max reconnection attempts reached')
|
||||||
this.connectionStatus.value = 'failed'
|
this.connectionStatus.value = 'failed'
|
||||||
|
|
||||||
// Show user notification about WebSocket issues
|
// Check if we should fallback to polling
|
||||||
if (this.toast) {
|
if (this.config.fallbackToPolling && !this.usingPollingFallback) {
|
||||||
this.toast.info('Real-time balance updates temporarily unavailable', {
|
console.log('WalletWebSocketService: Falling back to polling for balance updates')
|
||||||
description: 'WebSocket connection failed. Balance will update on page refresh.'
|
this.startPollingFallback()
|
||||||
})
|
} else {
|
||||||
|
// Show user notification about WebSocket issues
|
||||||
|
if (this.toast) {
|
||||||
|
this.toast.info('Real-time balance updates temporarily unavailable', {
|
||||||
|
description: 'WebSocket connection failed. Balance will update on page refresh.'
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -367,12 +419,20 @@ export class WalletWebSocketService extends BaseService {
|
||||||
this.reconnectTimer = null
|
this.reconnectTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.stabilityTimer) {
|
||||||
|
clearTimeout(this.stabilityTimer)
|
||||||
|
this.stabilityTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
if (this.ws) {
|
if (this.ws) {
|
||||||
// Close with normal closure code
|
// Close with normal closure code
|
||||||
this.ws.close(1000, 'Client disconnect')
|
this.ws.close(1000, 'Client disconnect')
|
||||||
this.ws = null
|
this.ws = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop polling fallback
|
||||||
|
this.stopPollingFallback()
|
||||||
|
|
||||||
this.isConnected.value = false
|
this.isConnected.value = false
|
||||||
this.connectionStatus.value = 'disconnected'
|
this.connectionStatus.value = 'disconnected'
|
||||||
}
|
}
|
||||||
|
|
@ -383,10 +443,13 @@ export class WalletWebSocketService extends BaseService {
|
||||||
private async onResume(): Promise<void> {
|
private async onResume(): Promise<void> {
|
||||||
console.log('WalletWebSocketService: Resuming connection')
|
console.log('WalletWebSocketService: Resuming connection')
|
||||||
|
|
||||||
// Reconnect if not connected
|
// Try WebSocket first, then fallback to polling if needed
|
||||||
if (!this.isConnected.value) {
|
if (!this.isConnected.value && !this.usingPollingFallback) {
|
||||||
this.reconnectAttempts = 0
|
this.reconnectAttempts = 0
|
||||||
await this.connectIfNeeded()
|
await this.connectIfNeeded()
|
||||||
|
} else if (this.usingPollingFallback) {
|
||||||
|
// Resume polling if we were using fallback
|
||||||
|
this.schedulePolling()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -396,8 +459,24 @@ export class WalletWebSocketService extends BaseService {
|
||||||
private async onPause(): Promise<void> {
|
private async onPause(): Promise<void> {
|
||||||
console.log('WalletWebSocketService: Pausing connection')
|
console.log('WalletWebSocketService: Pausing connection')
|
||||||
|
|
||||||
// Disconnect to save battery
|
// Disconnect WebSocket to save battery
|
||||||
this.disconnect()
|
if (this.ws) {
|
||||||
|
this.ws.close(1000, 'App paused')
|
||||||
|
this.ws = null
|
||||||
|
this.isConnected.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause polling to save battery
|
||||||
|
if (this.pollingTimer) {
|
||||||
|
clearTimeout(this.pollingTimer)
|
||||||
|
this.pollingTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear reconnect timers
|
||||||
|
if (this.reconnectTimer) {
|
||||||
|
clearTimeout(this.reconnectTimer)
|
||||||
|
this.reconnectTimer = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -437,6 +516,213 @@ export class WalletWebSocketService extends BaseService {
|
||||||
public async cleanup(): Promise<void> {
|
public async cleanup(): Promise<void> {
|
||||||
this.disconnect()
|
this.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start polling fallback when WebSocket fails persistently
|
||||||
|
*/
|
||||||
|
private startPollingFallback(): void {
|
||||||
|
if (this.usingPollingFallback) return
|
||||||
|
|
||||||
|
this.usingPollingFallback = true
|
||||||
|
this.connectionStatus.value = 'polling-fallback'
|
||||||
|
|
||||||
|
console.log(`WalletWebSocketService: Starting polling fallback (${this.config.pollingInterval}ms interval)`)
|
||||||
|
|
||||||
|
// Show user notification
|
||||||
|
if (this.toast) {
|
||||||
|
this.toast.info('Using backup connection', {
|
||||||
|
description: 'Real-time updates temporarily using polling instead of WebSocket.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.schedulePolling()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop polling fallback
|
||||||
|
*/
|
||||||
|
private stopPollingFallback(): void {
|
||||||
|
if (!this.usingPollingFallback) return
|
||||||
|
|
||||||
|
this.usingPollingFallback = false
|
||||||
|
|
||||||
|
if (this.pollingTimer) {
|
||||||
|
clearTimeout(this.pollingTimer)
|
||||||
|
this.pollingTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('WalletWebSocketService: Stopped polling fallback')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule next polling attempt
|
||||||
|
*/
|
||||||
|
private schedulePolling(): void {
|
||||||
|
if (!this.usingPollingFallback) return
|
||||||
|
|
||||||
|
this.pollingTimer = setTimeout(() => {
|
||||||
|
this.performPollingUpdate()
|
||||||
|
}, this.config.pollingInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a polling-based balance update
|
||||||
|
*/
|
||||||
|
private async performPollingUpdate(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const wallet = this.paymentService?.getPreferredWallet?.()
|
||||||
|
if (!wallet?.inkey) {
|
||||||
|
console.log('WalletWebSocketService: No wallet available for polling')
|
||||||
|
this.schedulePolling() // Continue polling
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch balance from LNbits API
|
||||||
|
const baseUrl = import.meta.env.VITE_LNBITS_BASE_URL || import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000'
|
||||||
|
const response = await fetch(`${baseUrl}/api/v1/wallet`, {
|
||||||
|
headers: {
|
||||||
|
'X-Api-Key': wallet.inkey
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('WalletWebSocketService: Polling failed', response.status, response.statusText)
|
||||||
|
this.schedulePolling() // Continue polling despite error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.balance !== undefined) {
|
||||||
|
// LNbits API returns balance in millisats
|
||||||
|
const balanceMsat = parseInt(data.balance)
|
||||||
|
console.log('WalletWebSocketService: Polling update - balance:', balanceMsat, 'msat')
|
||||||
|
|
||||||
|
// Update balance via PaymentService
|
||||||
|
if (this.paymentService?.updateWalletBalance) {
|
||||||
|
this.paymentService.updateWalletBalance(balanceMsat, wallet.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule next polling
|
||||||
|
this.schedulePolling()
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WalletWebSocketService: Polling error', error)
|
||||||
|
this.schedulePolling() // Continue polling despite error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced diagnostic handlers for WebSocket events
|
||||||
|
*/
|
||||||
|
private handleOpenWithDiagnostics(event: Event): void {
|
||||||
|
const now = Date.now()
|
||||||
|
console.log('WalletWebSocketService: WebSocket OPEN event', {
|
||||||
|
timestamp: now,
|
||||||
|
readyState: this.ws?.readyState,
|
||||||
|
url: this.ws?.url,
|
||||||
|
protocol: this.ws?.protocol,
|
||||||
|
extensions: this.ws?.extensions,
|
||||||
|
event: event
|
||||||
|
})
|
||||||
|
|
||||||
|
// Call original handler
|
||||||
|
this.handleOpen(event)
|
||||||
|
|
||||||
|
// Schedule a delayed check to see if connection stays open
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.ws) {
|
||||||
|
console.log('WalletWebSocketService: Connection health check (5s after open)', {
|
||||||
|
readyState: this.ws.readyState,
|
||||||
|
readyStateText: this.getReadyStateText(this.ws.readyState),
|
||||||
|
isConnected: this.isConnected.value,
|
||||||
|
status: this.connectionStatus.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMessageWithDiagnostics(event: MessageEvent): void {
|
||||||
|
console.log('WalletWebSocketService: WebSocket MESSAGE event', {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
dataLength: event.data?.length || 0,
|
||||||
|
dataType: typeof event.data,
|
||||||
|
readyState: this.ws?.readyState,
|
||||||
|
preview: event.data?.substring ? event.data.substring(0, 100) + '...' : event.data
|
||||||
|
})
|
||||||
|
|
||||||
|
// Call original handler
|
||||||
|
this.handleMessage(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCloseWithDiagnostics(event: CloseEvent): void {
|
||||||
|
const now = Date.now()
|
||||||
|
console.log('WalletWebSocketService: WebSocket CLOSE event', {
|
||||||
|
timestamp: now,
|
||||||
|
code: event.code,
|
||||||
|
reason: event.reason,
|
||||||
|
wasClean: event.wasClean,
|
||||||
|
readyState: this.ws?.readyState,
|
||||||
|
readyStateText: this.ws ? this.getReadyStateText(this.ws.readyState) : 'null',
|
||||||
|
closeCodeMeaning: this.getCloseCodeMeaning(event.code),
|
||||||
|
connectionDuration: this.getConnectionDuration(now)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Call original handler
|
||||||
|
this.handleClose(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleWebSocketErrorWithDiagnostics(event: Event): void {
|
||||||
|
console.error('WalletWebSocketService: WebSocket ERROR event', {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
readyState: this.ws?.readyState,
|
||||||
|
readyStateText: this.ws ? this.getReadyStateText(this.ws.readyState) : 'null',
|
||||||
|
url: this.ws?.url,
|
||||||
|
event: event,
|
||||||
|
networkStatus: navigator.onLine ? 'online' : 'offline',
|
||||||
|
userAgent: navigator.userAgent
|
||||||
|
})
|
||||||
|
|
||||||
|
// Call original handler
|
||||||
|
this.handleWebSocketError(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getReadyStateText(readyState: number): string {
|
||||||
|
switch (readyState) {
|
||||||
|
case WebSocket.CONNECTING: return 'CONNECTING (0)'
|
||||||
|
case WebSocket.OPEN: return 'OPEN (1)'
|
||||||
|
case WebSocket.CLOSING: return 'CLOSING (2)'
|
||||||
|
case WebSocket.CLOSED: return 'CLOSED (3)'
|
||||||
|
default: return `UNKNOWN (${readyState})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCloseCodeMeaning(code: number): string {
|
||||||
|
switch (code) {
|
||||||
|
case 1000: return 'Normal Closure'
|
||||||
|
case 1001: return 'Going Away'
|
||||||
|
case 1002: return 'Protocol Error'
|
||||||
|
case 1003: return 'Unsupported Data'
|
||||||
|
case 1005: return 'No Status Rcvd'
|
||||||
|
case 1006: return 'Abnormal Closure'
|
||||||
|
case 1007: return 'Invalid frame payload data'
|
||||||
|
case 1008: return 'Policy Violation'
|
||||||
|
case 1009: return 'Message Too Big'
|
||||||
|
case 1010: return 'Mandatory Extension'
|
||||||
|
case 1011: return 'Internal Server Error'
|
||||||
|
case 1015: return 'TLS Handshake'
|
||||||
|
default: return `Unknown Code (${code})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private connectionStartTime = 0
|
||||||
|
|
||||||
|
private getConnectionDuration(closeTime: number): string {
|
||||||
|
if (this.connectionStartTime === 0) return 'Unknown'
|
||||||
|
const duration = closeTime - this.connectionStartTime
|
||||||
|
return `${duration}ms`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WalletWebSocketService
|
export default WalletWebSocketService
|
||||||
Loading…
Add table
Add a link
Reference in a new issue