|
|
|
|
|
import { ref, onMounted, onUnmounted } from 'vue';
|
|
|
import { LRUCache } from 'lru-cache';
|
|
|
import dayjs from 'dayjs'
|
|
|
import { COMMAND } from '~/utils/commandTypes'
|
|
|
|
|
|
let heartbeatTimer: NodeJS.Timeout;
|
|
|
|
|
|
// 定义 WebSocket Composable 的返回类型
|
|
|
interface WebSocketInstance {
|
|
|
ws: Ref<WebSocket | null>
|
|
|
status: Ref<'connecting' | 'open' | 'error' | 'closed'>
|
|
|
messages: Ref<{ data: string; timestamp: number }[]>
|
|
|
send: (name: string, json: object, token: string, cmdid: string) => void
|
|
|
onMessage: (callback: (data: string) => void) => () => void
|
|
|
connect: () => void
|
|
|
close: () => void
|
|
|
}
|
|
|
|
|
|
let instance: WebSocketInstance | null = null;
|
|
|
|
|
|
export function useWebSocket() {
|
|
|
|
|
|
if (instance) {
|
|
|
return instance;
|
|
|
}
|
|
|
|
|
|
const ws = ref<WebSocket | null>(null);
|
|
|
const messages = ref<{ data: string; timestamp: number }[]>([]);
|
|
|
const eventCallbacks: Array<(data: string) => void> = [] // 存储onMessage 回调
|
|
|
const status = ref<'connecting' | 'open' | 'error' | 'closed'>('closed');
|
|
|
// const pendingMessages = ref([]);
|
|
|
// 存储 { msgId: timestamp }
|
|
|
// const receivedMessages = new Map(); // 使用 Map 更方便记录时间
|
|
|
|
|
|
// 清理超过 5 分钟的消息 ID
|
|
|
// const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 分钟
|
|
|
const EXPIRE_TIME = 5 * 60 * 1000;// 5 分钟
|
|
|
const reconnectAttempts = ref(0);
|
|
|
// const maxReconnectAttempts = 5;
|
|
|
const isConnected = computed(() => { return status.value === 'open' });
|
|
|
const isConnecting = computed(() => { return status.value === 'connecting' });
|
|
|
const config = useRuntimeConfig();
|
|
|
|
|
|
const connectionTimeout = config.public.apiTimeout || 5000; // 5秒连接超时
|
|
|
|
|
|
const cache = new LRUCache({
|
|
|
max: 1000, // 最多 1000 个条目
|
|
|
ttl: EXPIRE_TIME // 5 分钟过期
|
|
|
});
|
|
|
|
|
|
// 初始化连接
|
|
|
const connect = () => {
|
|
|
if (isConnected.value || isConnecting.value) return;
|
|
|
status.value = 'connecting';
|
|
|
ws.value = new WebSocket(config.public.wsUrl, "json");
|
|
|
|
|
|
ws.value.onopen = () => {
|
|
|
status.value = 'open';
|
|
|
reconnectAttempts.value = 0;
|
|
|
// retryPendingMessages();
|
|
|
};
|
|
|
|
|
|
ws.value.onmessage = (event) => {
|
|
|
if (!event.data) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const json = JSON.parse(event.data);
|
|
|
const evtType = json.type;
|
|
|
|
|
|
// 连接响应不做处理
|
|
|
if (evtType === COMMAND.PONG || evtType === COMMAND.CONNECTED) {
|
|
|
if (evtType === COMMAND.CONNECTED) {
|
|
|
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const msgId = `${evtType}-${json.dateTime}`;
|
|
|
|
|
|
// if (evtType !== COMMAND.SELECT_HANDLE) {
|
|
|
// console.log("msgId:", msgId);
|
|
|
// }
|
|
|
// const now = Date.now();
|
|
|
// 重复ID不做处理(排除选择事件)
|
|
|
if (cache.has(msgId) && (evtType !== COMMAND.SELECT_HANDLE && evtType !== COMMAND.DRAG_ELEMENT && evtType !== COMMAND.MAP_RANGE_REAL)) {
|
|
|
console.warn('Duplicate message ignored:', msgId, json);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
cache.set(msgId, true);
|
|
|
|
|
|
eventCallbacks.forEach(cb => cb(event.data));
|
|
|
if (messages.value.length > 100) messages.value.shift();
|
|
|
messages.value.push({ data: event.data, timestamp: Date.now() });
|
|
|
};
|
|
|
|
|
|
ws.value.onerror = (error) => {
|
|
|
status.value = 'error';
|
|
|
// console.error('WebSocket发生错误:', error);
|
|
|
|
|
|
// // 重试逻辑
|
|
|
// if (reconnectAttempts.value < maxReconnectAttempts) {
|
|
|
// setTimeout(connect, 1000 * reconnectAttempts.value);
|
|
|
// reconnectAttempts.value++;
|
|
|
// }
|
|
|
console.error('🚨 WebSocket 错误:', error)
|
|
|
ws.value?.close()
|
|
|
};
|
|
|
|
|
|
ws.value.onclose = (event) => {
|
|
|
status.value = 'closed';
|
|
|
if (!event.wasClean) {
|
|
|
// 非正常关闭时尝试重连
|
|
|
setTimeout(connect, 3000);
|
|
|
}
|
|
|
console.log("连接关闭:", event)
|
|
|
};
|
|
|
}
|
|
|
|
|
|
// 等待连接就绪
|
|
|
const waitForConnection = () => {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
if (isConnected.value) {
|
|
|
resolve(true);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
console.log("Timeout:", connectionTimeout);
|
|
|
// 设置连接超时
|
|
|
const timeout = setTimeout(() => {
|
|
|
reject(new Error('连接超时'));
|
|
|
}, connectionTimeout);
|
|
|
|
|
|
// 监听连接状态变化
|
|
|
const watchHandle = watch(isConnected, (newVal) => {
|
|
|
if (newVal) {
|
|
|
clearTimeout(timeout);
|
|
|
watchHandle(); // 取消监听
|
|
|
resolve(true);
|
|
|
}
|
|
|
}, { immediate: true });
|
|
|
});
|
|
|
};
|
|
|
|
|
|
// 发送消息
|
|
|
const send = async (name: string, json: object, token: string = '', cmdid: string = '') => {
|
|
|
const msg = {
|
|
|
type: name,
|
|
|
drawerToken: token || localStorage.getItem(cmdid) || '',
|
|
|
data: json,
|
|
|
cmdid: cmdid
|
|
|
}
|
|
|
try {
|
|
|
// 等待连接就绪
|
|
|
await waitForConnection();
|
|
|
// const ts = dayjs().unix();
|
|
|
// console.log("发送命令1:", msg);
|
|
|
// const msgId = `${name}-${msg.drawerToken}-${ts}`;
|
|
|
// if (cache.has(msgId)) {
|
|
|
// return;
|
|
|
// }
|
|
|
// console.log("发送命令2:", msg);
|
|
|
if (name !== COMMAND.PING && name !== COMMAND.MOUSE_MOVE) {
|
|
|
console.log("发送消息:", msg);
|
|
|
}
|
|
|
|
|
|
// cache.set(msgId, ts);
|
|
|
ws.value.send(JSON.stringify(msg));
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('消息发送失败:', error);
|
|
|
// 添加到待发队列
|
|
|
// pendingMessages.value.push(JSON.stringify({ type: name, data: json }));
|
|
|
// throw error;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 重发待处理消息
|
|
|
const retryPendingMessages = () => {
|
|
|
// while (pendingMessages.value.length > 0) {
|
|
|
// const message = pendingMessages.value.shift();
|
|
|
// let jsonData = JSON.parse(message);
|
|
|
// send(jsonData.type, jsonData.data);
|
|
|
// }
|
|
|
};
|
|
|
|
|
|
const startHeartbeat = () => {
|
|
|
heartbeatTimer = setInterval(() => {
|
|
|
if (ws.value?.readyState === WebSocket.OPEN) {
|
|
|
send(COMMAND.PING, {});
|
|
|
}
|
|
|
}, 5000)
|
|
|
}
|
|
|
|
|
|
const onMessage = (callback: any) => {
|
|
|
eventCallbacks.push(callback);
|
|
|
|
|
|
return () => {
|
|
|
const index = eventCallbacks.indexOf(callback);
|
|
|
if (index > -1) {
|
|
|
eventCallbacks.splice(index, 1);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// setInterval(() => {
|
|
|
// const now = Date.now();
|
|
|
// for (const [msgId, timestamp] of receivedMessages) {
|
|
|
// if (now - timestamp > EXPIRE_TIME) {
|
|
|
// receivedMessages.delete(msgId);
|
|
|
// }
|
|
|
// }
|
|
|
// }, CLEANUP_INTERVAL);
|
|
|
|
|
|
// const reconnect = ()=>{
|
|
|
// setTimeout(()=>{
|
|
|
// connect();
|
|
|
// },50000)
|
|
|
// }
|
|
|
|
|
|
// 手动关闭
|
|
|
const close = () => {
|
|
|
if (ws.value) {
|
|
|
ws.value.close()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
onMounted(() => {
|
|
|
if (status.value === 'closed') {
|
|
|
connect();
|
|
|
}
|
|
|
startHeartbeat();
|
|
|
});
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
// if (heartbeatTimer) {
|
|
|
// clearInterval(heartbeatTimer);
|
|
|
// };
|
|
|
// socket.value?.close();
|
|
|
});
|
|
|
|
|
|
// return { message, status, send, connect, waitForConnection }
|
|
|
|
|
|
instance = {
|
|
|
ws,
|
|
|
status,
|
|
|
messages,
|
|
|
send,
|
|
|
onMessage,
|
|
|
connect,
|
|
|
close
|
|
|
}
|
|
|
|
|
|
// 初始连接
|
|
|
if (typeof window !== 'undefined') {
|
|
|
window.addEventListener('load', connect)
|
|
|
}
|
|
|
|
|
|
return instance;
|
|
|
} |