Merge branch 'websocket-wallet-tx-updates'
This commit is contained in:
commit
fe7ed67946
2 changed files with 307 additions and 19 deletions
|
|
@ -78,7 +78,9 @@ export const appConfig: AppConfig = {
|
|||
websocket: {
|
||||
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)
|
||||
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
|
||||
reconnectDelay: number
|
||||
maxReconnectAttempts: number
|
||||
fallbackToPolling: boolean
|
||||
pollingInterval: number
|
||||
}
|
||||
|
||||
export class WalletWebSocketService extends BaseService {
|
||||
|
|
@ -21,10 +23,16 @@ export class WalletWebSocketService extends BaseService {
|
|||
private ws: WebSocket | null = null
|
||||
private reconnectTimer: NodeJS.Timeout | null = null
|
||||
private reconnectAttempts = 0
|
||||
private pollingTimer: NodeJS.Timeout | null = null
|
||||
private stabilityTimer: NodeJS.Timeout | null = null
|
||||
private usingPollingFallback = false
|
||||
private consecutiveFailures = 0
|
||||
private config: WebSocketConfig = {
|
||||
enabled: true,
|
||||
reconnectDelay: 1000,
|
||||
maxReconnectAttempts: 5
|
||||
maxReconnectAttempts: 5,
|
||||
fallbackToPolling: true,
|
||||
pollingInterval: 10000 // 10 seconds
|
||||
}
|
||||
|
||||
// Service dependencies (auto-injected by BaseService)
|
||||
|
|
@ -143,14 +151,22 @@ export class WalletWebSocketService extends BaseService {
|
|||
|
||||
this.connectionStatus.value = 'connecting'
|
||||
|
||||
// Create WebSocket connection
|
||||
// Create WebSocket connection with detailed monitoring
|
||||
this.ws = new WebSocket(wsUrl)
|
||||
|
||||
// Set up event handlers
|
||||
this.ws.onopen = this.handleOpen.bind(this)
|
||||
this.ws.onmessage = this.handleMessage.bind(this)
|
||||
this.ws.onclose = this.handleClose.bind(this)
|
||||
this.ws.onerror = this.handleWebSocketError.bind(this)
|
||||
// Log initial WebSocket state
|
||||
console.log('WalletWebSocketService: WebSocket created', {
|
||||
readyState: this.ws.readyState,
|
||||
url: this.ws.url,
|
||||
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...')
|
||||
|
||||
|
|
@ -165,10 +181,12 @@ export class WalletWebSocketService extends BaseService {
|
|||
* Handle WebSocket connection opened
|
||||
*/
|
||||
private handleOpen(_event: Event): void {
|
||||
// Record connection start time for duration tracking
|
||||
this.connectionStartTime = Date.now()
|
||||
|
||||
console.log('WalletWebSocketService: Connected successfully')
|
||||
this.isConnected.value = true
|
||||
this.connectionStatus.value = 'connected'
|
||||
this.reconnectAttempts = 0
|
||||
|
||||
// Clear any pending reconnect timer
|
||||
if (this.reconnectTimer) {
|
||||
|
|
@ -176,6 +194,26 @@ export class WalletWebSocketService extends BaseService {
|
|||
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
|
||||
this.sendPing()
|
||||
}
|
||||
|
|
@ -285,6 +323,12 @@ export class WalletWebSocketService extends BaseService {
|
|||
this.connectionStatus.value = 'disconnected'
|
||||
this.ws = null
|
||||
|
||||
// Clear stability timer since connection closed
|
||||
if (this.stabilityTimer) {
|
||||
clearTimeout(this.stabilityTimer)
|
||||
this.stabilityTimer = null
|
||||
}
|
||||
|
||||
// Handle specific close codes
|
||||
if (event.code === 1006) {
|
||||
console.warn('WalletWebSocketService: Abnormal closure detected - possible server issue')
|
||||
|
|
@ -321,16 +365,24 @@ export class WalletWebSocketService extends BaseService {
|
|||
* Schedule a reconnection attempt
|
||||
*/
|
||||
private scheduleReconnect(isAbnormalClosure = false): void {
|
||||
this.consecutiveFailures++
|
||||
|
||||
// Don't reconnect if we've exceeded max attempts
|
||||
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'
|
||||
|
||||
// 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.'
|
||||
})
|
||||
// Check if we should fallback to polling
|
||||
if (this.config.fallbackToPolling && !this.usingPollingFallback) {
|
||||
console.log('WalletWebSocketService: Falling back to polling for balance updates')
|
||||
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
|
||||
}
|
||||
|
|
@ -367,12 +419,20 @@ export class WalletWebSocketService extends BaseService {
|
|||
this.reconnectTimer = null
|
||||
}
|
||||
|
||||
if (this.stabilityTimer) {
|
||||
clearTimeout(this.stabilityTimer)
|
||||
this.stabilityTimer = null
|
||||
}
|
||||
|
||||
if (this.ws) {
|
||||
// Close with normal closure code
|
||||
this.ws.close(1000, 'Client disconnect')
|
||||
this.ws = null
|
||||
}
|
||||
|
||||
// Stop polling fallback
|
||||
this.stopPollingFallback()
|
||||
|
||||
this.isConnected.value = false
|
||||
this.connectionStatus.value = 'disconnected'
|
||||
}
|
||||
|
|
@ -383,10 +443,13 @@ export class WalletWebSocketService extends BaseService {
|
|||
private async onResume(): Promise<void> {
|
||||
console.log('WalletWebSocketService: Resuming connection')
|
||||
|
||||
// Reconnect if not connected
|
||||
if (!this.isConnected.value) {
|
||||
// Try WebSocket first, then fallback to polling if needed
|
||||
if (!this.isConnected.value && !this.usingPollingFallback) {
|
||||
this.reconnectAttempts = 0
|
||||
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> {
|
||||
console.log('WalletWebSocketService: Pausing connection')
|
||||
|
||||
// Disconnect to save battery
|
||||
this.disconnect()
|
||||
// Disconnect WebSocket to save battery
|
||||
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> {
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue