You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

262 lines
7.9 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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("Event Data", event.data);
}
// 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, {});
}
}, 10000)
}
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;
}