Files
websocket-qris/examples/clientsocket/composables/useWebSocket.fixed
2025-09-24 18:42:16 +07:00

1074 lines
28 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ref, computed, reactive, type Ref } from "vue";
import type {
WebSocketMessage,
ConnectionState,
WebSocketConfig,
MessageHistory,
ConnectionStats,
MonitoringData,
ClientInfo,
OnlineUser,
ActivityLog,
} from "../types/websocket";
export const useWebSocket = () => {
// Check if we're in browser environment
const isBrowser = process.client;
// WebSocket connection
const ws: Ref<WebSocket | null> = ref(null);
// Connection state - consolidated into single reactive object
const connectionState = reactive<ConnectionState>({
isConnected: false,
isConnecting: false,
connectionStatus: "disconnected",
clientId: null,
staticId: null,
currentRoom: null,
userId: "anonymous",
ipAddress: null,
connectionStartTime: null,
lastPingTime: null,
connectionLatency: 0,
connectionHealth: "poor",
reconnectAttempts: 0,
messagesReceived: 0,
messagesSent: 0,
uptime: "00:00:00",
});
// Configuration
const config = reactive<WebSocketConfig>({
wsUrl: "ws://localhost:8080/api/v1/ws",
userId: "anonymous",
room: "default",
staticId: "",
useIPBasedId: false,
autoReconnect: true,
heartbeatEnabled: true,
maxReconnectAttempts: 10,
reconnectDelay: 1000,
maxReconnectDelay: 30000,
heartbeatInterval: 30000,
heartbeatTimeout: 5000,
maxMissedHeartbeats: 3,
maxMessages: 1000,
messageWarningThreshold: 800,
actionThrottle: 100,
});
// Message history and data
const messages = ref<MessageHistory[]>([]);
const stats = ref<ConnectionStats | null>(null);
const monitoringData = ref<MonitoringData | null>(null);
const onlineUsers = ref<OnlineUser[]>([]);
const activityLog = ref<ActivityLog[]>([]);
// Admin functionality
const serverInfo = ref<any>(null);
const systemHealth = ref<any>(null);
// Timer references for proper cleanup
let reconnectTimeout: number | null = null;
let heartbeatInterval: number | null = null;
let heartbeatTimeout: number | null = null;
let uptimeInterval: number | null = null;
let connectionStartTime: number | null = null;
// Heartbeat tracking
let missedHeartbeats = 0;
let lastHeartbeatTime = 0;
let isManualDisconnect = false;
// Helper function to update connection status
const updateConnectionStatus = (
status: "disconnected" | "connecting" | "connected" | "error"
) => {
connectionState.connectionStatus = status;
switch (status) {
case "connected":
connectionState.isConnected = true;
connectionState.isConnecting = false;
connectionState.connectionHealth = "good";
break;
case "connecting":
connectionState.isConnected = false;
connectionState.isConnecting = true;
connectionState.connectionHealth = "warning";
break;
case "disconnected":
case "error":
connectionState.isConnected = false;
connectionState.isConnecting = false;
connectionState.connectionHealth = "poor";
break;
}
};
// Helper function to update uptime
const updateUptime = () => {
if (connectionStartTime && connectionState.isConnected) {
const now = Date.now();
const uptime = now - connectionStartTime;
const seconds = Math.floor(uptime / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
connectionState.uptime = `${hours.toString().padStart(2, "0")}:${(
minutes % 60
)
.toString()
.padStart(2, "0")}:${(seconds % 60).toString().padStart(2, "0")}`;
} else {
connectionState.uptime = "00:00:00";
}
};
// Timer management
const startUptimeTimer = () => {
if (uptimeInterval) {
clearInterval(uptimeInterval);
}
uptimeInterval = window.setInterval(updateUptime, 1000);
};
const stopUptimeTimer = () => {
if (uptimeInterval) {
clearInterval(uptimeInterval);
uptimeInterval = null;
}
};
// Message handling
const getIconForMessageType = (type: string): string => {
switch (type) {
case "success":
return "🟢";
case "error":
return "❌";
case "warning":
return "⚠️";
case "info":
return "";
default:
return "📝";
}
};
const addMessage = (
type: string,
data: any,
messageId?: string,
icon?: string
) => {
if (!isBrowser) return;
const now = new Date();
const timeString = now.toLocaleTimeString("en-US", {
hour12: true,
hour: "numeric",
minute: "2-digit",
second: "2-digit",
});
// Enhanced message formatting based on type
let formattedData = data;
let displayType = type;
switch (type) {
case "connection_info":
displayType = "info";
formattedData = {
client_id: data.clientId,
connected_at: data.connectedAt,
ip_address: data.ipAddress,
last_ping: data.lastPingTime || "N/A",
room: data.room || "default",
static_id: data.staticId || "none",
user_id: data.userId || "anonymous",
};
break;
case "heartbeat":
displayType = "info";
formattedData = `❤️ Heartbeat started\nInterval: ${
config.heartbeatInterval / 1000
}s`;
break;
case "pong":
displayType = "success";
formattedData = `🏓 Pong received\nLatency: ${connectionState.connectionLatency}ms`;
break;
case "message":
displayType = "info";
formattedData = `📨 Message received\n${JSON.stringify(data, null, 2)}`;
break;
case "broadcast":
displayType = "info";
formattedData = `📡 Broadcast received\n${data}`;
break;
case "direct_message":
displayType = "info";
formattedData = `📨 Direct message from ${data.from}\n${data.message}`;
break;
case "room_message":
displayType = "info";
formattedData = `📢 Room message from ${data.room}\n${data.message}`;
break;
case "stats":
displayType = "info";
formattedData = `📊 Stats updated\n${JSON.stringify(data, null, 2)}`;
break;
case "monitoring":
displayType = "info";
formattedData = `📈 Monitoring data\n${JSON.stringify(data, null, 2)}`;
break;
case "online_users":
displayType = "info";
formattedData = `👥 Online users updated\nCount: ${
data.users?.length || 0
}`;
break;
case "server_info":
displayType = "info";
formattedData = `🔧 Server info\n${JSON.stringify(data, null, 2)}`;
break;
case "system_health":
displayType = "info";
formattedData = `💚 System health\n${JSON.stringify(data, null, 2)}`;
break;
default:
if (typeof data === "string") {
formattedData = data;
} else {
formattedData = JSON.stringify(data, null, 2);
}
}
const message: MessageHistory = {
timestamp: now,
type: displayType,
data: formattedData,
messageId,
size: JSON.stringify(data).length,
icon: icon || getIconForMessageType(displayType),
timeString: timeString,
};
messages.value.unshift(message);
// Keep only the last maxMessages
if (messages.value.length > config.maxMessages) {
messages.value = messages.value.slice(0, config.maxMessages);
}
// Update connection state
connectionState.messagesReceived++;
};
// Computed properties
const connectionHealthColor = computed(() => {
switch (connectionState.connectionHealth) {
case "excellent":
return "#4CAF50";
case "good":
return "#2196F3";
case "warning":
return "#FFC107";
case "poor":
return "#F44336";
default:
return "#9E9E9E";
}
});
const connectionHealthText = computed(() => {
switch (connectionState.connectionHealth) {
case "excellent":
return "Excellent";
case "good":
return "Good";
case "warning":
return "Warning";
case "poor":
return "Poor";
default:
return "Unknown";
}
});
const isMessageLimitReached = computed(() => {
return messages.value.length >= config.maxMessages;
});
const shouldShowMessageWarning = computed(() => {
return messages.value.length >= config.messageWarningThreshold;
});
// Timer cleanup
const cleanupTimers = () => {
if (!isBrowser) return;
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
reconnectTimeout = null;
}
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
if (heartbeatTimeout) {
clearTimeout(heartbeatTimeout);
heartbeatTimeout = null;
}
stopUptimeTimer();
};
// Connection cleanup
const cleanupConnection = () => {
if (!isBrowser) return;
if (ws.value) {
ws.value.close();
ws.value = null;
}
cleanupTimers();
};
// Get close reason description
const getCloseReason = (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 1004:
return "Reserved";
case 1005:
return "No status received";
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 1012:
return "Service restart";
case 1013:
return "Try again later";
case 1014:
return "Bad gateway";
case 1015:
return "TLS handshake";
default:
return "Unknown reason";
}
};
// Heartbeat management
const stopHeartbeat = () => {
if (!isBrowser) return;
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
if (heartbeatTimeout) {
clearTimeout(heartbeatTimeout);
heartbeatTimeout = null;
}
missedHeartbeats = 0;
};
const startHeartbeat = () => {
if (!isBrowser || !config.heartbeatEnabled) return;
stopHeartbeat();
heartbeatInterval = window.setInterval(() => {
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
const heartbeatMessage = {
type: "ping",
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(heartbeatMessage));
lastHeartbeatTime = Date.now();
// Set timeout for heartbeat response
heartbeatTimeout = window.setTimeout(() => {
missedHeartbeats++;
if (missedHeartbeats >= config.maxMissedHeartbeats) {
addMessage(
"❌ Heartbeat timeout - connection unhealthy",
"warning"
);
connectionState.connectionHealth = "warning";
}
}, config.heartbeatTimeout);
}
}, config.heartbeatInterval);
};
// Reconnection logic
const scheduleReconnect = () => {
if (!isBrowser || !config.autoReconnect) return;
if (connectionState.reconnectAttempts >= config.maxReconnectAttempts) {
addMessage("❌ Max reconnection attempts reached", "error");
return;
}
connectionState.reconnectAttempts++;
const delay = Math.min(
config.reconnectDelay *
Math.pow(2, connectionState.reconnectAttempts - 1),
config.maxReconnectDelay
);
addMessage(
`🔄 Reconnecting in ${delay}ms (attempt ${connectionState.reconnectAttempts}/${config.maxReconnectAttempts})`,
"info"
);
reconnectTimeout = window.setTimeout(() => {
connect();
}, delay);
};
// Message handling
const handleIncomingMessage = (event: MessageEvent) => {
if (!isBrowser) return;
try {
const data = JSON.parse(event.data);
// Handle heartbeat response
if (data.type === "pong") {
missedHeartbeats = 0;
connectionState.connectionHealth = "excellent";
const latency = Date.now() - lastHeartbeatTime;
connectionState.connectionLatency = latency;
return;
}
// Handle connection info
if (data.type === "connection_info") {
connectionState.clientId = data.clientId;
connectionState.staticId = data.staticId;
connectionState.currentRoom = data.room;
connectionState.userId = data.userId;
connectionState.ipAddress = data.ipAddress;
return;
}
// Handle welcome message (server sends connection info in welcome message)
if (data.type === "welcome") {
console.log("Received welcome message:", data);
// Map server snake_case fields to camelCase for Vue components
if (data.client_id) {
connectionState.clientId = data.client_id;
console.log("Set clientId:", data.client_id);
}
if (data.static_id) {
connectionState.staticId = data.static_id;
console.log("Set staticId:", data.static_id);
}
if (data.room) {
connectionState.currentRoom = data.room;
console.log("Set currentRoom:", data.room);
}
if (data.user_id) {
connectionState.userId = data.user_id;
console.log("Set userId:", data.user_id);
}
if (data.ip_address) {
connectionState.ipAddress = data.ip_address;
console.log("Set ipAddress:", data.ip_address);
}
if (data.connected_at) {
connectionState.connectionStartTime = data.connected_at * 1000;
console.log("Set connectionStartTime:", data.connected_at * 1000);
}
// Force reactive update by triggering a change
connectionState.connectionHealth =
connectionState.connectionHealth === "good" ? "excellent" : "good";
console.log("Updated connectionState:", connectionState);
addMessage(
"🟢 Connection established successfully",
"success",
"Welcome message processed"
);
return;
}
// Handle stats
if (data.type === "stats") {
stats.value = data;
return;
}
// Handle monitoring data
if (data.type === "monitoring") {
monitoringData.value = data;
return;
}
// Handle online users
if (data.type === "online_users") {
onlineUsers.value = data.users;
return;
}
// Handle activity log
if (data.type === "activity") {
activityLog.value.unshift(data);
if (activityLog.value.length > 100) {
activityLog.value = activityLog.value.slice(0, 100);
}
return;
}
// Handle server info
if (data.type === "server_info") {
serverInfo.value = data;
return;
}
// Handle system health
if (data.type === "system_health") {
systemHealth.value = data;
return;
}
// Handle regular messages
addMessage(data.type || "message", data, data.messageId);
} catch (error) {
addMessage(
"❌ Failed to parse message",
"error",
error instanceof Error ? error.message : String(error)
);
}
};
// WebSocket connection methods
const connect = () => {
if (!isBrowser) return;
// Prevent multiple connection attempts
if (connectionState.isConnecting || connectionState.isConnected) {
return;
}
// Clean up existing connection
if (ws.value) {
cleanupConnection();
}
updateConnectionStatus("connecting");
isManualDisconnect = false;
connectionStartTime = Date.now();
startUptimeTimer();
try {
// Build WebSocket URL with parameters
let url = config.wsUrl;
const params = new URLSearchParams();
if (config.userId) {
params.append("user_id", config.userId);
}
if (config.room) {
params.append("room", config.room);
}
if (config.useIPBasedId) {
params.append("ip_based", "true");
} else if (config.staticId) {
params.append("static_id", config.staticId);
}
if (params.toString()) {
url += "?" + params.toString();
}
ws.value = new WebSocket(url);
// Connection timeout
const connectionTimeout = setTimeout(() => {
if (ws.value && ws.value.readyState === WebSocket.CONNECTING) {
ws.value.close();
updateConnectionStatus("error");
addMessage("❌ Connection timeout after 15 seconds", "error");
scheduleReconnect();
}
}, 15000);
ws.value.onopen = function (event) {
clearTimeout(connectionTimeout);
updateConnectionStatus("connected");
connectionState.reconnectAttempts = 0;
connectionState.connectionStartTime = connectionStartTime;
addMessage(
"🟢 Connected to WebSocket server",
"success",
"Connection established successfully"
);
// Start heartbeat if enabled
if (config.heartbeatEnabled) {
startHeartbeat();
}
};
ws.value.onmessage = function (event) {
handleIncomingMessage(event);
};
ws.value.onclose = function (event) {
clearTimeout(connectionTimeout);
cleanupTimers();
updateConnectionStatus("disconnected");
const reason = getCloseReason(event.code);
addMessage(
`🔴 Disconnected: ${reason}`,
"error",
`Code: ${event.code}, Reason: ${event.reason || "Unknown"}`
);
// Auto-reconnect logic
if (!isManualDisconnect && config.autoReconnect) {
scheduleReconnect();
}
};
ws.value.onerror = function (error) {
updateConnectionStatus("error");
addMessage("❌ WebSocket Error occurred", "error", error.toString());
};
} catch (error) {
updateConnectionStatus("error");
addMessage(
"❌ Failed to create WebSocket connection",
"error",
error instanceof Error ? error.message : String(error)
);
scheduleReconnect();
}
};
const disconnect = () => {
if (!isBrowser) return;
isManualDisconnect = true;
cleanupConnection();
updateConnectionStatus("disconnected");
addMessage("🔴 Manually disconnected from WebSocket", "info");
};
// Message sending methods
const sendMessage = (message: any) => {
if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) {
addMessage("❌ Cannot send message: not connected", "error");
return false;
}
try {
const messageData = {
type: "message",
data: message,
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(messageData));
connectionState.messagesSent++;
addMessage("📤 Message sent", "info", message);
return true;
} catch (error) {
addMessage(
"❌ Failed to send message",
"error",
error instanceof Error ? error.message : String(error)
);
return false;
}
};
const broadcastMessage = (message: string) => {
if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) {
addMessage("❌ Cannot broadcast message: not connected", "error");
return false;
}
try {
const messageData = {
type: "broadcast",
message,
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(messageData));
connectionState.messagesSent++;
addMessage("📡 Message broadcasted", "info", message);
return true;
} catch (error) {
addMessage(
"❌ Failed to broadcast message",
"error",
error instanceof Error ? error.message : String(error)
);
return false;
}
};
const sendDirectMessage = (clientId: string, message: string) => {
if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) {
addMessage("❌ Cannot send direct message: not connected", "error");
return false;
}
try {
const messageData = {
type: "direct_message",
clientId,
message,
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(messageData));
connectionState.messagesSent++;
addMessage(`📨 Direct message sent to ${clientId}`, "info", message);
return true;
} catch (error) {
addMessage(
"❌ Failed to send direct message",
"error",
error instanceof Error ? error.message : String(error)
);
return false;
}
};
const sendRoomMessage = (room: string, message: string) => {
if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) {
addMessage("❌ Cannot send room message: not connected", "error");
return false;
}
try {
const messageData = {
type: "room_message",
room,
message,
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(messageData));
connectionState.messagesSent++;
addMessage(`📢 Room message sent to ${room}`, "info", message);
return true;
} catch (error) {
addMessage(
"❌ Failed to send room message",
"error",
error instanceof Error ? error.message : String(error)
);
return false;
}
};
// Admin and utility methods
const executeAdminCommand = async (command: string, params: any) => {
if (!isBrowser || !ws.value) throw new Error("Not connected");
const message = {
type: "admin_command",
command,
params,
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(message));
return { success: true, message: "Command sent successfully" };
};
const getServerInfo = async () => {
if (!isBrowser || !ws.value) throw new Error("Not connected");
const message = {
type: "get_server_info",
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(message));
};
const getSystemHealth = async () => {
if (!isBrowser || !ws.value) throw new Error("Not connected");
const message = {
type: "get_system_health",
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(message));
};
const getOnlineUsers = () => {
if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) {
addMessage("❌ Cannot get online users: not connected", "error");
return;
}
try {
const message = {
type: "get_online_users",
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(message));
} catch (error) {
addMessage(
"❌ Failed to request online users",
"error",
error instanceof Error ? error.message : String(error)
);
}
};
const testConnection = () => {
if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) {
addMessage("❌ Cannot test connection: not connected", "error");
return;
}
try {
const message = {
type: "ping",
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(message));
addMessage("🏓 Connection test sent", "info");
} catch (error) {
addMessage(
"❌ Failed to test connection",
"error",
error instanceof Error ? error.message : String(error)
);
}
};
const sendHeartbeat = () => {
if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) {
addMessage("❌ Cannot send heartbeat: not connected", "error");
return;
}
try {
const message = {
type: "ping",
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(message));
lastHeartbeatTime = Date.now();
} catch (error) {
addMessage(
"❌ Failed to send heartbeat",
"error",
error instanceof Error ? error.message : String(error)
);
}
};
const executeDatabaseQuery = async (query: string) => {
if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) {
throw new Error("Not connected");
}
try {
const message = {
type: "database_query",
query,
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(message));
return { success: true, message: "Database query sent successfully" };
} catch (error) {
throw new Error(
`Failed to execute database query: ${
error instanceof Error ? error.message : String(error)
}`
);
}
};
const triggerNotification = async (message: string) => {
if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) {
throw new Error("Not connected");
}
try {
const notificationData = {
type: "notification",
message,
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(notificationData));
return { success: true, message: "Notification sent successfully" };
} catch (error) {
throw new Error(
`Failed to trigger notification: ${
error instanceof Error ? error.message : String(error)
}`
);
}
};
const getStats = () => {
if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) {
addMessage("❌ Cannot get stats: not connected", "error");
return;
}
try {
const message = {
type: "get_stats",
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(message));
} catch (error) {
addMessage(
"❌ Failed to request stats",
"error",
error instanceof Error ? error.message : String(error)
);
}
};
const getMonitoringData = () => {
if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) {
addMessage("❌ Cannot get monitoring data: not connected", "error");
return;
}
try {
const message = {
type: "get_monitoring",
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(message));
} catch (error) {
addMessage(
"❌ Failed to request monitoring data",
"error",
error instanceof Error ? error.message : String(error)
);
}
};
// Utility methods
const clearMessages = () => {
if (!isBrowser) return;
messages.value = [];
};
const clearActivityLog = () => {
if (!isBrowser) return;
activityLog.value = [];
};
const getMessagesByType = (type: string) => {
return messages.value.filter((msg) => msg.type === type);
};
const getRecentMessages = (count: number = 10) => {
return messages.value.slice(0, count);
};
// Cleanup on unmount
const cleanup = () => {
if (!isBrowser) return;
disconnect();
cleanupTimers();
};
return {
// State
ws,
connectionState,
config,
messages,
stats,
monitoringData,
onlineUsers,
activityLog,
// Admin state
serverInfo,
systemHealth,
// Methods
connect,
disconnect,
sendMessage,
broadcastMessage,
sendDirectMessage,
sendRoomMessage,
getServerInfo,
getOnlineUsers,
testConnection,
sendHeartbeat,
executeDatabaseQuery,
triggerNotification,
getStats,
getMonitoringData,
executeAdminCommand,
getSystemHealth,
clearMessages,
clearActivityLog,
getMessagesByType,
getRecentMessages,
cleanup,
// Computed
isMessageLimitReached,
shouldShowMessageWarning,
connectionHealthColor,
connectionHealthText,
};
};