206 lines
6.1 KiB
TypeScript
206 lines
6.1 KiB
TypeScript
// composables/useWebSocket.ts
|
|
import { ref, computed, onUnmounted, getCurrentInstance, toValue } from 'vue'
|
|
|
|
export interface WebSocketConfig {
|
|
url: string | any
|
|
clientId: string | any
|
|
onMessage?: (data: any) => void
|
|
onOpen?: () => void
|
|
onClose?: () => void
|
|
onError?: (error: Event) => void
|
|
reconnectInterval?: number
|
|
maxReconnectAttempts?: number
|
|
fallbackPostUrl?: string // Optional: use a specific URL for the POST fallback (e.g., a proxy)
|
|
}
|
|
|
|
export interface WebSocketMessage {
|
|
to_client: string
|
|
data: any
|
|
}
|
|
|
|
export const useWebSocket = (config: WebSocketConfig) => {
|
|
const ws = ref<WebSocket | null>(null)
|
|
const isConnected = ref(false)
|
|
const reconnectAttempts = ref(0)
|
|
const reconnectTimer = ref<NodeJS.Timeout | null>(null)
|
|
|
|
const currentUrl = computed(() => toValue(config.url))
|
|
const currentClientId = computed(() => toValue(config.clientId))
|
|
|
|
const wsUrl = computed(() => {
|
|
let baseUrl = currentUrl.value
|
|
|
|
// Automatically upgrade to secure protocols if caller uses HTTPS
|
|
if (typeof window !== 'undefined' && window.location.protocol === 'https:') {
|
|
if (baseUrl.startsWith('ws://')) {
|
|
baseUrl = baseUrl.replace('ws://', 'wss://')
|
|
} else if (baseUrl.startsWith('http://')) {
|
|
baseUrl = baseUrl.replace('http://', 'https://')
|
|
}
|
|
}
|
|
|
|
const url = new URL(baseUrl)
|
|
url.searchParams.set('client_id', currentClientId.value)
|
|
return url.toString()
|
|
})
|
|
|
|
const clearHandlers = (wsInstance: WebSocket | null) => {
|
|
if (wsInstance) {
|
|
wsInstance.onopen = null
|
|
wsInstance.onmessage = null
|
|
wsInstance.onclose = null
|
|
wsInstance.onerror = null
|
|
}
|
|
}
|
|
|
|
const connect = () => {
|
|
try {
|
|
// Clear any pending reconnect timer first
|
|
if (reconnectTimer.value) {
|
|
clearTimeout(reconnectTimer.value)
|
|
reconnectTimer.value = null
|
|
}
|
|
|
|
// Close existing connection if any, and CLEAN HANDLERS to prevent recursion
|
|
if (ws.value) {
|
|
if (ws.value.readyState !== WebSocket.CLOSED) {
|
|
console.log('🔌 Closing existing WebSocket before new connection...')
|
|
clearHandlers(ws.value)
|
|
ws.value.close()
|
|
}
|
|
ws.value = null
|
|
}
|
|
|
|
const connectionUrl = wsUrl.value
|
|
ws.value = new WebSocket(connectionUrl)
|
|
|
|
ws.value.onopen = () => {
|
|
console.log('✅ WebSocket connected:', currentClientId.value)
|
|
isConnected.value = true
|
|
reconnectAttempts.value = 0
|
|
config.onOpen?.()
|
|
}
|
|
|
|
ws.value.onmessage = (event) => {
|
|
try {
|
|
const data = JSON.parse(event.data)
|
|
config.onMessage?.(data)
|
|
} catch (error) {
|
|
console.error('Error parsing WebSocket message:', error)
|
|
}
|
|
}
|
|
|
|
ws.value.onclose = () => {
|
|
console.log('❌ WebSocket closed:', currentClientId.value)
|
|
isConnected.value = false
|
|
config.onClose?.()
|
|
|
|
// Attempt to reconnect only if we didn't just reach max attempts
|
|
// The reconnectTimer is cleared in disconnect() and at start of connect()
|
|
if (reconnectAttempts.value < (config.maxReconnectAttempts || 5)) {
|
|
reconnectAttempts.value++
|
|
const interval = config.reconnectInterval || 3000
|
|
console.log(`⏳ Reconnecting in ${interval}ms... Attempt ${reconnectAttempts.value}`)
|
|
reconnectTimer.value = setTimeout(() => {
|
|
connect()
|
|
}, interval)
|
|
} else {
|
|
console.error('Max reconnection attempts reached')
|
|
}
|
|
}
|
|
|
|
ws.value.onerror = (error) => {
|
|
console.error('⚠️ WebSocket error:', error)
|
|
config.onError?.(error)
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Error creating WebSocket:', error)
|
|
config.onError?.(error as Event)
|
|
}
|
|
}
|
|
|
|
const disconnect = () => {
|
|
if (reconnectTimer.value) {
|
|
clearTimeout(reconnectTimer.value)
|
|
reconnectTimer.value = null
|
|
}
|
|
if (ws.value) {
|
|
console.log('🔌 Manual disconnect: Cleaning handlers and closing...')
|
|
clearHandlers(ws.value)
|
|
ws.value.close()
|
|
ws.value = null
|
|
}
|
|
isConnected.value = false
|
|
reconnectAttempts.value = 0
|
|
}
|
|
|
|
const sendMessage = (message: any) => {
|
|
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
|
ws.value.send(JSON.stringify(message))
|
|
return true
|
|
}
|
|
console.warn('WebSocket is not connected')
|
|
return false
|
|
}
|
|
|
|
// Send data via POST to WebSocket server
|
|
const sendViaPost = async (message: WebSocketMessage) => {
|
|
try {
|
|
let postUrl = ''
|
|
|
|
if (config.fallbackPostUrl) {
|
|
// Use the explicitly provided fallback URL (likely a relative proxy path)
|
|
postUrl = config.fallbackPostUrl
|
|
} else {
|
|
// Derive post URL from WebSocket URL (legacy behavior)
|
|
let baseUrl = currentUrl.value
|
|
if (baseUrl.startsWith('ws://')) {
|
|
baseUrl = baseUrl.replace('ws://', 'http://')
|
|
} else if (baseUrl.startsWith('wss://')) {
|
|
baseUrl = baseUrl.replace('wss://', 'https://')
|
|
}
|
|
|
|
const urlObj = new URL(baseUrl)
|
|
postUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`
|
|
}
|
|
|
|
const response = await fetch(postUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(message),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text().catch(() => 'Unknown error')
|
|
console.error('❌ POST Error response:', errorText)
|
|
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`)
|
|
}
|
|
|
|
const result = await response.json().catch(() => ({ success: true }))
|
|
return result
|
|
} catch (error) {
|
|
console.error('❌ Error sending message via POST:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
if (getCurrentInstance()) {
|
|
onUnmounted(() => {
|
|
disconnect()
|
|
})
|
|
}
|
|
|
|
return {
|
|
ws: computed(() => ws.value),
|
|
isConnected: computed(() => isConnected.value),
|
|
connect,
|
|
disconnect,
|
|
sendMessage,
|
|
sendViaPost,
|
|
reconnectAttempts: computed(() => reconnectAttempts.value),
|
|
}
|
|
}
|
|
|