- Increased reconnect delay from 1 second to 2 seconds and reduced max reconnect attempts from 5 to 3 to minimize server load. - Improved logging messages for connection status and abnormal closures, providing clearer insights during reconnection attempts. - Added a ping mechanism to test connection stability and handle network connectivity issues more effectively. These changes enhance the reliability and performance of the WebSocket service, contributing to a better user experience.
441 lines
No EOL
14 KiB
TypeScript
441 lines
No EOL
14 KiB
TypeScript
import { BaseService } from '@/core/base/BaseService'
|
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
|
import { useToast } from '@/core/composables/useToast'
|
|
import { eventBus } from '@/core/event-bus'
|
|
import type { Ref } from 'vue'
|
|
import { ref } from 'vue'
|
|
|
|
interface WebSocketConfig {
|
|
enabled: boolean
|
|
reconnectDelay: number
|
|
maxReconnectAttempts: number
|
|
}
|
|
|
|
export class WalletWebSocketService extends BaseService {
|
|
protected readonly metadata = {
|
|
name: 'WalletWebSocketService',
|
|
version: '1.0.0',
|
|
dependencies: ['PaymentService', 'AuthService', 'WalletService']
|
|
}
|
|
|
|
private ws: WebSocket | null = null
|
|
private reconnectTimer: NodeJS.Timeout | null = null
|
|
private reconnectAttempts = 0
|
|
private config: WebSocketConfig = {
|
|
enabled: true,
|
|
reconnectDelay: 1000,
|
|
maxReconnectAttempts: 5
|
|
}
|
|
|
|
// Service dependencies (auto-injected by BaseService)
|
|
protected paymentService: any
|
|
protected walletService: any
|
|
|
|
// WebSocket state
|
|
public readonly isConnected: Ref<boolean> = ref(false)
|
|
public readonly connectionStatus: Ref<string> = ref('disconnected')
|
|
|
|
private toast = useToast()
|
|
|
|
protected async onInitialize(): Promise<void> {
|
|
console.log('WalletWebSocketService: Starting initialization...')
|
|
|
|
// Get services (already injected by BaseService)
|
|
this.paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE) as any
|
|
this.walletService = injectService(SERVICE_TOKENS.WALLET_SERVICE) as any
|
|
|
|
console.log('WalletWebSocketService: Services injected', {
|
|
paymentService: !!this.paymentService,
|
|
walletService: !!this.walletService
|
|
})
|
|
|
|
// Load config from app config if available
|
|
const appConfig = (window as any).appConfig
|
|
if (appConfig?.modules?.wallet?.config?.websocket) {
|
|
this.config = { ...this.config, ...appConfig.modules.wallet.config.websocket }
|
|
}
|
|
|
|
console.log('WalletWebSocketService: Config loaded', this.config)
|
|
|
|
// Only proceed if WebSocket is enabled
|
|
if (!this.config.enabled) {
|
|
console.log('WalletWebSocketService: WebSocket disabled in config')
|
|
return
|
|
}
|
|
|
|
// Register with VisibilityService for connection management
|
|
if (this.visibilityService) {
|
|
this.visibilityService.registerService(
|
|
this.metadata.name,
|
|
this.onResume.bind(this),
|
|
this.onPause.bind(this)
|
|
)
|
|
console.log('WalletWebSocketService: Registered with VisibilityService')
|
|
} else {
|
|
console.warn('WalletWebSocketService: VisibilityService not available')
|
|
}
|
|
|
|
// Listen for authentication events to connect when user logs in
|
|
eventBus.on('auth:login', () => {
|
|
console.log('WalletWebSocketService: Auth login detected, attempting connection...')
|
|
setTimeout(() => {
|
|
// Small delay to ensure PaymentService has processed the auth event
|
|
this.connectIfNeeded()
|
|
}, 500)
|
|
})
|
|
|
|
eventBus.on('auth:logout', () => {
|
|
console.log('WalletWebSocketService: Auth logout detected, disconnecting...')
|
|
this.disconnect()
|
|
})
|
|
|
|
console.log('WalletWebSocketService: Registered auth event listeners')
|
|
|
|
// Initial connection attempt (will fail if not authenticated, but that's OK)
|
|
console.log('WalletWebSocketService: Attempting initial connection...')
|
|
await this.connectIfNeeded()
|
|
}
|
|
|
|
/**
|
|
* Connect to LNbits WebSocket if we have wallet credentials
|
|
*/
|
|
private async connectIfNeeded(): Promise<void> {
|
|
console.log('WalletWebSocketService: Checking for wallet credentials...')
|
|
|
|
const wallet = this.paymentService?.getPreferredWallet?.()
|
|
console.log('WalletWebSocketService: Wallet check result', {
|
|
hasPaymentService: !!this.paymentService,
|
|
hasGetPreferredWallet: !!this.paymentService?.getPreferredWallet,
|
|
wallet: wallet ? { id: wallet.id, hasInkey: !!wallet.inkey } : null
|
|
})
|
|
|
|
if (wallet?.inkey) {
|
|
console.log('WalletWebSocketService: Connecting with wallet inkey...')
|
|
await this.connect(wallet.inkey)
|
|
} else {
|
|
console.log('WalletWebSocketService: No wallet available for WebSocket connection')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Establish WebSocket connection to LNbits
|
|
*/
|
|
private async connect(walletInkey: string): Promise<void> {
|
|
try {
|
|
console.log('WalletWebSocketService: Starting connection process...')
|
|
|
|
// Close existing connection if any
|
|
this.disconnect()
|
|
|
|
// Build WebSocket URL
|
|
const baseUrl = import.meta.env.VITE_LNBITS_BASE_URL || import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000'
|
|
const wsProtocol = baseUrl.startsWith('https') ? 'wss:' : 'ws:'
|
|
const host = baseUrl.replace(/^https?:\/\//, '').replace(/\/$/, '')
|
|
const wsUrl = `${wsProtocol}//${host}/api/v1/ws/${walletInkey}`
|
|
|
|
console.log('WalletWebSocketService: Connection details', {
|
|
baseUrl,
|
|
wsProtocol,
|
|
host,
|
|
wsUrl,
|
|
walletInkey: walletInkey.substring(0, 8) + '...' // Only show first 8 chars for security
|
|
})
|
|
|
|
this.connectionStatus.value = 'connecting'
|
|
|
|
// Create WebSocket connection
|
|
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)
|
|
|
|
console.log('WalletWebSocketService: WebSocket created, waiting for connection...')
|
|
|
|
} catch (error) {
|
|
console.error('WalletWebSocketService: Failed to create WebSocket', error)
|
|
this.connectionStatus.value = 'error'
|
|
this.scheduleReconnect()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle WebSocket connection opened
|
|
*/
|
|
private handleOpen(_event: Event): void {
|
|
console.log('WalletWebSocketService: Connected successfully')
|
|
this.isConnected.value = true
|
|
this.connectionStatus.value = 'connected'
|
|
this.reconnectAttempts = 0
|
|
|
|
// Clear any pending reconnect timer
|
|
if (this.reconnectTimer) {
|
|
clearTimeout(this.reconnectTimer)
|
|
this.reconnectTimer = null
|
|
}
|
|
|
|
// Send a ping to test connection stability
|
|
this.sendPing()
|
|
}
|
|
|
|
/**
|
|
* Handle incoming WebSocket messages
|
|
*/
|
|
private handleMessage(event: MessageEvent): void {
|
|
try {
|
|
const data = JSON.parse(event.data)
|
|
console.log('WalletWebSocketService: Received message', data)
|
|
|
|
// Handle payment notification first (this updates transactions)
|
|
if (data.payment) {
|
|
this.handlePaymentNotification(data.payment)
|
|
}
|
|
|
|
// Handle wallet balance update
|
|
if (data.wallet_balance !== undefined) {
|
|
console.log('WalletWebSocketService: Processing balance update', {
|
|
newBalance: data.wallet_balance,
|
|
hasPayment: !!data.payment,
|
|
paymentAmount: data.payment?.amount
|
|
})
|
|
|
|
let finalBalance = data.wallet_balance
|
|
|
|
// For outgoing payments, LNbits sends pre-payment balance, so we need to adjust
|
|
// For incoming payments, LNbits sends post-payment balance, so use as-is
|
|
if (data.payment && data.payment.amount < 0) {
|
|
// Outgoing payment - subtract the payment amount from the balance
|
|
const paymentSats = Math.abs(data.payment.amount) / 1000
|
|
finalBalance = data.wallet_balance - paymentSats
|
|
console.log('WalletWebSocketService: Adjusting balance for outgoing payment', {
|
|
originalBalance: data.wallet_balance,
|
|
paymentSats: paymentSats,
|
|
finalBalance: finalBalance
|
|
})
|
|
} else if (data.payment && data.payment.amount > 0) {
|
|
// Incoming payment - use balance as-is (already post-payment)
|
|
console.log('WalletWebSocketService: Using balance as-is for incoming payment', {
|
|
balance: data.wallet_balance
|
|
})
|
|
} else {
|
|
// No payment in message - use balance as-is
|
|
console.log('WalletWebSocketService: Using balance as-is (no payment)', {
|
|
balance: data.wallet_balance
|
|
})
|
|
}
|
|
|
|
console.log('WalletWebSocketService: Updating balance to', finalBalance, 'sats')
|
|
this.updateWalletBalance(finalBalance)
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('WalletWebSocketService: Failed to parse message', error)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update wallet balance via PaymentService
|
|
*/
|
|
private updateWalletBalance(balanceSats: number): void {
|
|
console.log('WalletWebSocketService: Updating balance to', balanceSats, 'sats')
|
|
|
|
// Convert sats to millisats for internal storage (LNbits WebSocket sends balance in sats)
|
|
const balanceMsat = balanceSats * 1000
|
|
|
|
// Get the wallet ID we're connected to
|
|
const wallet = this.paymentService?.getPreferredWallet?.()
|
|
const walletId = wallet?.id
|
|
|
|
// Update balance via PaymentService (which manages wallet state)
|
|
if (this.paymentService?.updateWalletBalance) {
|
|
this.paymentService.updateWalletBalance(balanceMsat, walletId)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle incoming payment notification
|
|
*/
|
|
private handlePaymentNotification(payment: any): void {
|
|
console.log('WalletWebSocketService: Payment notification', payment)
|
|
|
|
// Add transaction to wallet service
|
|
if (this.walletService?.addTransaction) {
|
|
this.walletService.addTransaction(payment)
|
|
}
|
|
|
|
// Show toast notification for incoming payments
|
|
if (payment.amount > 0 && !payment.pending) {
|
|
const amountSats = Math.abs(payment.amount / 1000)
|
|
if (payment.amount > 0) {
|
|
this.toast.success(`Received ${amountSats} sats!`)
|
|
} else {
|
|
this.toast.info(`Sent ${amountSats} sats`)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle WebSocket connection closed
|
|
*/
|
|
private handleClose(event: CloseEvent): void {
|
|
console.log('WalletWebSocketService: Connection closed', event.code, event.reason)
|
|
this.isConnected.value = false
|
|
this.connectionStatus.value = 'disconnected'
|
|
this.ws = null
|
|
|
|
// Handle specific close codes
|
|
if (event.code === 1006) {
|
|
console.warn('WalletWebSocketService: Abnormal closure detected - possible server issue')
|
|
// For code 1006, increase delay to avoid overwhelming server
|
|
this.scheduleReconnect(true)
|
|
} else if (event.code !== 1000) {
|
|
// Normal reconnection for other non-normal closures
|
|
this.scheduleReconnect(false)
|
|
}
|
|
// Code 1000 = normal closure, don't reconnect
|
|
}
|
|
|
|
/**
|
|
* Handle WebSocket error
|
|
*/
|
|
private handleWebSocketError(event: Event): void {
|
|
console.error('WalletWebSocketService: WebSocket error', event)
|
|
this.connectionStatus.value = 'error'
|
|
|
|
// Additional error details if available
|
|
if (this.ws) {
|
|
console.error('WalletWebSocketService: WebSocket state:', this.ws.readyState)
|
|
console.error('WalletWebSocketService: WebSocket URL:', this.ws.url)
|
|
}
|
|
|
|
// Check if this is a network connectivity issue
|
|
if (!navigator.onLine) {
|
|
console.log('WalletWebSocketService: Network appears to be offline')
|
|
this.connectionStatus.value = 'offline'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule a reconnection attempt
|
|
*/
|
|
private scheduleReconnect(isAbnormalClosure = false): void {
|
|
// Don't reconnect if we've exceeded max attempts
|
|
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
console.log('WalletWebSocketService: Max reconnection attempts reached - disabling WebSocket')
|
|
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.'
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
// Clear any existing reconnect timer
|
|
if (this.reconnectTimer) {
|
|
clearTimeout(this.reconnectTimer)
|
|
}
|
|
|
|
// Calculate delay with exponential backoff
|
|
let delay = this.config.reconnectDelay * Math.pow(2, this.reconnectAttempts)
|
|
|
|
// For abnormal closures (1006), use longer delays to avoid overwhelming server
|
|
if (isAbnormalClosure) {
|
|
delay = Math.max(delay, 5000) // Minimum 5 second delay for 1006 errors
|
|
}
|
|
|
|
this.reconnectAttempts++
|
|
|
|
console.log(`WalletWebSocketService: Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.config.maxReconnectAttempts})${isAbnormalClosure ? ' [abnormal closure]' : ''}`)
|
|
this.connectionStatus.value = 'reconnecting'
|
|
|
|
this.reconnectTimer = setTimeout(() => {
|
|
this.connectIfNeeded()
|
|
}, delay)
|
|
}
|
|
|
|
/**
|
|
* Disconnect WebSocket connection
|
|
*/
|
|
public disconnect(): void {
|
|
if (this.reconnectTimer) {
|
|
clearTimeout(this.reconnectTimer)
|
|
this.reconnectTimer = null
|
|
}
|
|
|
|
if (this.ws) {
|
|
// Close with normal closure code
|
|
this.ws.close(1000, 'Client disconnect')
|
|
this.ws = null
|
|
}
|
|
|
|
this.isConnected.value = false
|
|
this.connectionStatus.value = 'disconnected'
|
|
}
|
|
|
|
/**
|
|
* Resume connection when app becomes visible
|
|
*/
|
|
private async onResume(): Promise<void> {
|
|
console.log('WalletWebSocketService: Resuming connection')
|
|
|
|
// Reconnect if not connected
|
|
if (!this.isConnected.value) {
|
|
this.reconnectAttempts = 0
|
|
await this.connectIfNeeded()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pause connection when app loses visibility
|
|
*/
|
|
private async onPause(): Promise<void> {
|
|
console.log('WalletWebSocketService: Pausing connection')
|
|
|
|
// Disconnect to save battery
|
|
this.disconnect()
|
|
}
|
|
|
|
/**
|
|
* Send ping to test connection
|
|
*/
|
|
private sendPing(): void {
|
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
try {
|
|
// Send a ping frame (most WebSocket implementations support this)
|
|
this.ws.ping?.()
|
|
} catch (error) {
|
|
console.log('WalletWebSocketService: Ping not supported, connection seems stable')
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Manual reconnection method
|
|
*/
|
|
public async reconnect(): Promise<void> {
|
|
console.log('WalletWebSocketService: Manual reconnection triggered')
|
|
this.reconnectAttempts = 0
|
|
|
|
// Disconnect current connection if any
|
|
this.disconnect()
|
|
|
|
// Wait a moment before reconnecting
|
|
setTimeout(() => {
|
|
this.connectIfNeeded()
|
|
}, 1000)
|
|
}
|
|
|
|
/**
|
|
* Clean up on service destruction
|
|
*/
|
|
public async cleanup(): Promise<void> {
|
|
this.disconnect()
|
|
}
|
|
}
|
|
|
|
export default WalletWebSocketService |