Files
websocket-qris/examples/clientsocket/composables/note
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, nextTick } 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;
const ws = ref<WebSocket | null>(null);
const isConnected = ref(false);
const isConnecting = ref(false);
const connectionStatus = ref<
"disconnected" | "connecting" | "connected" | "error"
>("disconnected");
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",
});
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,
});
const messages = ref<MessageHistory[]>([]);
const stats = ref<ConnectionStats | null>(null);
const monitoringData = ref<MonitoringData | null>(null);
const onlineUsers = ref<OnlineUser[]>([]);
const activityLog = ref<ActivityLog[]>([]);
let reconnectTimeout: number | null = null;
let heartbeatInterval: number | null = null;
let heartbeatTimeout: number | null = null;
let missedHeartbeats = 0;
let lastHeartbeatTime = 0;
let messageCount = 0;
let connectionStartTime: number | null = null;
let uptimeInterval: number | null = null;
let isManualDisconnect = false;
let reconnectAttempts = 0;
let connectionHealth: "excellent" | "good" | "warning" | "poor" = "poor";
const updateConnectionStatus = (
status: "disconnected" | "connecting" | "connected" | "error"
) => {
connectionStatus.value = status;
connectionState.connectionStatus = status;
switch (status) {
case "connected":
isConnected.value = true;
isConnecting.value = false;
connectionState.isConnected = true;
connectionState.isConnecting = false;
connectionState.connectionHealth = "good";
break;
case "connecting":
isConnected.value = false;
isConnecting.value = true;
connectionState.isConnected = false;
connectionState.isConnecting = true;
connectionState.connectionHealth = "warning";
break;
case "disconnected":
isConnected.value = false;
isConnecting.value = false;
connectionState.isConnected = false;
connectionState.isConnecting = false;
connectionState.connectionHealth = "poor";
break;
case "error":
isConnected.value = false;
isConnecting.value = false;
connectionState.isConnected = false;
connectionState.isConnecting = false;
connectionState.connectionHealth = "poor";
break;
}
};
const updateUptime = () => {
if (connectionStartTime && isConnected.value) {
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";
}
};
const startUptimeTimer = () => {
if (uptimeInterval) {
clearInterval(uptimeInterval);
}
uptimeInterval = window.setInterval(updateUptime, 1000);
};
const stopUptimeTimer = () => {
if (uptimeInterval) {
clearInterval(uptimeInterval);
uptimeInterval = null;
}
};
// Only run WebSocket logic in browser
if (isBrowser) {
// Initialize connection state
updateConnectionStatus("disconnected");
}
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);
messageCount++;
// Keep only the last maxMessages
if (messages.value.length > config.maxMessages) {
messages.value = messages.value.slice(0, config.maxMessages);
}
// Update connection state
connectionState.messagesReceived++;
};
const getIconForMessageType = (type: string): string => {
switch (type) {
case "success":
return "🟢";
case "error":
return "❌";
case "warning":
return "⚠️";
case "info":
return "";
default:
return "📝";
}
};
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";
}
});
// Admin functionality
const serverInfo = ref<any>(null);
const systemHealth = ref<any>(null);
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));
};
// Cleanup on unmount
const cleanup = () => {
if (!isBrowser) return;
disconnect();
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
}
stopHeartbeat();
};
// WebSocket connection methods (only available in browser)
const connect = () => {
if (!isBrowser) return;
// Prevent multiple connection attempts
if (isConnecting.value || isConnected.value) {
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");
reconnectAttempts = 0;
connectionHealth = "good";
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");
connectionHealth = "poor";
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");
};
const sendMessage = (message: any) => {
if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) {
addMessage("❌ Cannot send message: not connected", "error");
return;
}
try {
const messageData = {
type: "message",
data: message,
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(messageData));
connectionState.messagesSent++;
addMessage("📤 Message sent", "info", message);
} catch (error) {
addMessage(
"❌ Failed to send message",
"error",
error instanceof Error ? error.message : String(error)
);
}
};
const broadcastMessage = (message: string) => {
if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) {
addMessage("❌ Cannot broadcast message: not connected", "error");
return;
}
try {
const messageData = {
type: "broadcast",
message,
timestamp: Date.now(),
};
ws.value.send(JSON.stringify(messageData));
connectionState.messagesSent++;
addMessage("📡 Message broadcasted", "info", message);
} catch (error) {
addMessage(
"❌ Failed to broadcast message",
"error",
error instanceof Error ? error.message : String(error)
);
}
};
const sendDirectMessage = (clientId: string, message: string) => {
if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) {
addMessage("❌ Cannot send direct message: not connected", "error");
return;
}
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);
} catch (error) {
addMessage(
"❌ Failed to send direct message",
"error",
error instanceof Error ? error.message : String(error)
);
}
};
const sendRoomMessage = (room: string, message: string) => {
if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) {
addMessage("❌ Cannot send room message: not connected", "error");
return;
}
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);
} catch (error) {
addMessage(
"❌ Failed to send room message",
"error",
error instanceof Error ? error.message : String(error)
);
}
};
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)
);
}
};
const clearMessages = () => {
if (!isBrowser) return;
messages.value = [];
messageCount = 0;
};
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);
};
const stopHeartbeat = () => {
if (!isBrowser) return;
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
if (heartbeatTimeout) {
clearTimeout(heartbeatTimeout);
heartbeatTimeout = null;
}
missedHeartbeats = 0;
};
const cleanupConnection = () => {
if (!isBrowser) return;
if (ws.value) {
ws.value.close();
ws.value = null;
}
cleanupTimers();
};
const cleanupTimers = () => {
if (!isBrowser) return;
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
reconnectTimeout = null;
}
stopHeartbeat();
stopUptimeTimer();
};
const scheduleReconnect = () => {
if (!isBrowser || !config.autoReconnect) return;
if (reconnectAttempts >= config.maxReconnectAttempts) {
addMessage("❌ Max reconnection attempts reached", "error");
return;
}
reconnectAttempts++;
const delay = Math.min(
config.reconnectDelay * Math.pow(2, reconnectAttempts - 1),
config.maxReconnectDelay
);
addMessage(
`🔄 Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${config.maxReconnectAttempts})`,
"info"
);
reconnectTimeout = window.setTimeout(() => {
connect();
}, delay);
};
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"
);
connectionHealth = "warning";
}
}, config.heartbeatTimeout);
}
}, config.heartbeatInterval);
};
const handleIncomingMessage = (event: MessageEvent) => {
if (!isBrowser) return;
try {
const data = JSON.parse(event.data);
// Handle heartbeat response
if (data.type === "pong") {
missedHeartbeats = 0;
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)
);
}
};
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";
}
};
const isMessageLimitReached = computed(() => {
return messages.value.length >= config.maxMessages;
});
const shouldShowMessageWarning = computed(() => {
return messages.value.length >= config.messageWarningThreshold;
});
return {
// State
ws,
isConnected,
isConnecting,
connectionStatus,
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,
};
};