1074 lines
28 KiB
Plaintext
1074 lines
28 KiB
Plaintext
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,
|
||
};
|
||
};
|