fix(common): 🐛 apply issue (#318)

This commit is contained in:
Dawn
2025-08-19 19:13:02 +08:00
committed by GitHub
parent a50cc5f31c
commit 2ac3568d15
14 changed files with 159 additions and 1422 deletions

View File

@@ -4,8 +4,11 @@ use futures_util::{sink::SinkExt, stream::StreamExt};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use tauri::{AppHandle, Emitter};
use tokio::sync::{Mutex, RwLock, mpsc};
use tokio::task::JoinHandle;
use tokio::time::{Duration, interval, sleep};
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
use tracing::{debug, error, info, warn};
use url::Url;
@@ -43,6 +46,12 @@ pub struct WebSocketClient {
// 连接互斥锁,防止并发连接
connection_mutex: Arc<Mutex<()>>,
// 任务句柄管理
task_handles: Arc<RwLock<Vec<JoinHandle<()>>>>,
// 关闭信号发送器
close_sender: Arc<RwLock<Option<mpsc::UnboundedSender<()>>>>,
}
impl WebSocketClient {
@@ -66,6 +75,8 @@ impl WebSocketClient {
background_heartbeat_failures: Arc::new(AtomicU32::new(0)),
is_ws_connected: Arc::new(AtomicBool::new(false)),
connection_mutex: Arc::new(Mutex::new(())),
task_handles: Arc::new(RwLock::new(Vec::new())),
close_sender: Arc::new(RwLock::new(None)),
}
}
@@ -105,6 +116,23 @@ impl WebSocketClient {
// 更新连接状态
self.is_ws_connected.store(false, Ordering::SeqCst);
// 取消所有任务
let mut handles = self.task_handles.write().await;
let task_count = handles.len();
for handle in handles.drain(..) {
handle.abort();
}
info!("🛑 已取消 {} 个异步任务", task_count);
// 发送关闭信号以主动关闭 WebSocket 连接
if let Some(close_sender) = self.close_sender.write().await.take() {
if let Err(_) = close_sender.send(()) {
warn!("⚠️ 发送关闭信号失败,连接可能已经关闭");
} else {
info!("📤 已发送 WebSocket 关闭信号");
}
}
// 清理消息发送器
*self.message_sender.write().await = None;
@@ -116,6 +144,8 @@ impl WebSocketClient {
self.consecutive_failures.store(0, Ordering::SeqCst);
self.reconnect_attempts.store(0, Ordering::SeqCst);
self.heartbeat_active.store(false, Ordering::SeqCst);
info!("✅ WebSocket 连接已完全断开");
}
/// 发送消息
@@ -303,6 +333,10 @@ impl WebSocketClient {
let (msg_sender, mut msg_receiver) = mpsc::unbounded_channel();
*self.message_sender.write().await = Some(msg_sender);
// 创建关闭信号通道
let (close_sender, mut close_receiver) = mpsc::unbounded_channel();
*self.close_sender.write().await = Some(close_sender);
// 更新连接状态
self.update_state(
ConnectionState::Connected,
@@ -333,6 +367,15 @@ impl WebSocketClient {
break;
}
}
Some(_) = close_receiver.recv() => {
info!("🔒 收到关闭信号,主动关闭 WebSocket 连接");
if let Err(e) = ws_sender.close().await {
warn!("⚠️ 关闭 WebSocket 连接时出错: {}", e);
} else {
info!("✅ WebSocket 连接已主动关闭");
}
break;
}
else => break,
}
}
@@ -390,7 +433,7 @@ impl WebSocketClient {
let heartbeat_active = self.heartbeat_active.clone();
let message_sender_ref = self.message_sender.clone();
tokio::spawn(async move {
let monitor_task = tokio::spawn(async move {
// 等待任务完成或停止信号
tokio::select! {
_ = message_sender_task => {
@@ -413,6 +456,10 @@ impl WebSocketClient {
*message_sender_ref.write().await = None;
});
// 保存监控任务句柄
let mut handles = self.task_handles.write().await;
handles.push(monitor_task);
info!("✅ WebSocket 连接和后台任务已启动");
Ok(())
}
@@ -507,10 +554,6 @@ impl WebSocketClient {
info!("💬 收到消息");
let _ = app_handle.emit("ws-receive-message", data);
}
"joinGroup" => {
info!("🔄 加入群聊");
let _ = app_handle.emit("ws-join-group", data);
}
"msgRecall" => {
info!("🔄 消息撤回");
let _ = app_handle.emit("ws-msg-recall", data);
@@ -535,9 +578,9 @@ impl WebSocketClient {
}
// 好友相关
"requestNewFriend" => {
info!("👥 新好友申请");
let _ = app_handle.emit("ws-request-new-friend", data);
"newApply" => {
info!("👥 新的Apply申请");
let _ = app_handle.emit("ws-request-new-apply", data);
}
"requestApprovalFriend" => {
info!("✅ 同意好友申请");
@@ -716,7 +759,9 @@ impl WebSocketClient {
})
};
tokio::spawn(heartbeat_task);
// 保存心跳任务句柄
let mut handles = self.task_handles.write().await;
handles.push(heartbeat_task);
}
/// 发送待发消息

View File

@@ -77,14 +77,14 @@
</n-virtual-list>
<!-- 加载更多提示 -->
<n-flex v-if="contactStore.requestFriendsOptions.isLoading" justify="center" class="py-10px">
<n-flex v-if="contactStore.applyPageOptions.isLoading" justify="center" class="py-10px">
<n-spin size="small" />
<span class="text-(12px [--text-color]) ml-8px">加载中...</span>
</n-flex>
<!-- 没有更多数据提示 -->
<n-flex
v-else-if="contactStore.requestFriendsOptions.isLast && contactStore.requestFriendsList.length > 0"
v-else-if="contactStore.applyPageOptions.isLast && contactStore.requestFriendsList.length > 0"
justify="center"
class="py-10px">
<span class="text-(12px [--text-color])">没有更多好友申请了</span>
@@ -180,13 +180,13 @@ const handleScroll = (e: Event) => {
// 加载更多好友申请
const loadMoreFriendRequests = async () => {
// 如果已经是最后一页或正在加载中,则不再加载
if (contactStore.requestFriendsOptions.isLast || contactStore.requestFriendsOptions.isLoading) {
if (contactStore.applyPageOptions.isLast || contactStore.applyPageOptions.isLoading) {
return
}
isLoadingMore.value = true
try {
await contactStore.getRequestFriendsList(false)
await contactStore.getApplyPage(false)
} finally {
isLoadingMore.value = false
}
@@ -230,7 +230,7 @@ const handleFriendAction = async (action: string, applyId: string) => {
onMounted(() => {
// 组件挂载时刷新一次列表
contactStore.getRequestFriendsList(true)
contactStore.getApplyPage(true)
})
</script>

View File

@@ -309,13 +309,18 @@ useMitt.on(WsResponseMessageType.RECEIVE_MESSAGE, async (data: MessageType) => {
await globalStore.updateGlobalUnreadCount()
})
useMitt.on(WsResponseMessageType.REQUEST_NEW_FRIEND, async (data: { uid: number; unreadCount: number }) => {
console.log('收到好友申请', data.unreadCount)
// 更新未读数
globalStore.unReadMark.newFriendUnreadCount += data.unreadCount
// 刷新好友申请列表
await contactStore.getRequestFriendsList(true)
})
useMitt.on(
WsResponseMessageType.REQUEST_NEW_FRIEND,
async (data: { uid: number; unReadCount4Friend: number; unReadCount4Group: number }) => {
console.log('收到好友申请')
// 更新未读数
globalStore.unReadMark.newFriendUnreadCount = data.unReadCount4Friend || 0
globalStore.unReadMark.newGroupUnreadCount = data.unReadCount4Group || 0
// 刷新好友申请列表
await contactStore.getApplyPage(true)
}
)
useMitt.on(
WsResponseMessageType.NEW_FRIEND_SESSION,
(param: {
@@ -366,7 +371,7 @@ useMitt.on(WsResponseMessageType.ROOM_DISSOLUTION, async () => {
onBeforeMount(async () => {
// 默认执行一次
await contactStore.getContactList(true)
await contactStore.getRequestFriendsList(true)
await contactStore.getApplyPage(true)
await contactStore.getGroupChatList()
})

View File

@@ -55,11 +55,7 @@
</svg>
</n-badge>
<!-- 好友提示 -->
<n-badge
v-if="item.url === 'friendsList'"
:max="99"
:value="unReadMark.newFriendUnreadCount"
:show="unReadMark.newFriendUnreadCount > 0">
<n-badge v-if="item.url === 'friendsList'" :max="99" :value="unreadApplyCount" :show="unreadApplyCount > 0">
<svg class="size-22px">
<use
:href="`#${activeUrl === item.url || openWindowsList.has(item.url) ? item.iconAction : item.icon}`"></use>
@@ -299,6 +295,10 @@ const handleTipShow = (item: any) => {
item.dot = false
}
const unreadApplyCount = computed(() => {
return globalStore.unReadMark.newFriendUnreadCount + globalStore.unReadMark.newGroupUnreadCount
})
const startResize = () => {
window.dispatchEvent(new Event('resize'))
}

View File

@@ -78,7 +78,7 @@ export default {
/** 获取联系人列表 */
getContactList: (params?: any) => GET<ListResponse<ContactItem>>(urls.getContactList, params),
/** 获取好友申请列表 */
requestFriendList: (params?: any) => GET<ListResponse<RequestFriendItem>>(urls.requestFriendList, params),
getApplyPage: (params?: any) => GET<ListResponse<RequestFriendItem>>(urls.requestApplyPage, params),
/** 发送添加好友请求 */
sendAddFriendRequest: (params: { targetUid: string; msg: string }) => POST(urls.sendAddFriendRequest, params),
/** 同意邀请进群或好友申请1 同意 2 拒绝 */
@@ -86,7 +86,7 @@ export default {
/** 删除好友 */
deleteFriend: (params: { targetUid: string }) => DELETE(urls.deleteFriend, params),
/** 好友申请未读数 */
newFriendCount: () => GET<{ unReadCount: number }>(urls.newFriendCount),
applyUnReadCount: () => GET<{ unReadCount4Friend: number; unReadCount4Group: number }>(urls.applyUnReadCount),
/** 会话列表 */
// getSessionList: (params?: any) => GET<ListResponse<SessionItem>>(urls.getSessionList, params),
/** 消息的已读未读列表 */

View File

@@ -42,8 +42,8 @@ export default {
// -------------- 好友相关 ---------------
getContactList: `${prefix + URLEnum.USER}/friend/page`, // 联系人列表
requestFriendList: `${prefix + URLEnum.ROOM}/apply/page`, // 好友申请、群聊邀请列表
newFriendCount: `${prefix + URLEnum.ROOM}/apply/unread`, // 申请未读数
requestApplyPage: `${prefix + URLEnum.ROOM}/apply/page`, // 好友申请、群聊邀请列表
applyUnReadCount: `${prefix + URLEnum.ROOM}/apply/unread`, // 申请未读数
handleInvite: `${prefix + URLEnum.ROOM}/apply/handler/apply`, // 审批别人邀请的进群
sendAddFriendRequest: `${prefix + URLEnum.ROOM}/apply/apply`, // 申请好友\同意申请
deleteFriend: `${prefix + URLEnum.USER}/friend`, // 删除好友

View File

@@ -1,749 +0,0 @@
import { listen } from '@tauri-apps/api/event'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { info } from '@tauri-apps/plugin-log'
import { useDebounceFn } from '@vueuse/core'
import { type ChangeTypeEnum, ConnectionState, type OnlineEnum, URLEnum, WorkerMsgEnum } from '@/enums'
import { useMitt } from '@/hooks/useMitt.ts'
import type { useNetworkReconnect as UseNetworkReconnectType } from '@/hooks/useNetworkReconnect'
import { useTauriListener } from '@/hooks/useTauriListener'
import { getEnhancedFingerprint } from '@/services/fingerprint.ts'
// 使用类型导入避免直接执行代码
import type { MarkItemType, MessageType, RevokedMsgType } from '@/services/types'
import type {
CallResponseData,
LoginInitResType,
LoginSuccessResType,
OnStatusChangeType,
RoomActionData,
SignalData,
UserStateType,
VideoCallRequestData,
WsReqMsgContentType
} from '@/services/wsType.ts'
import { WsResponseMessageType, type WsTokenExpire } from '@/services/wsType.ts'
import { useUserStore } from '@/stores/user'
const { addListener } = useTauriListener()
// 创建 webSocket worker
const worker: Worker = new Worker(new URL('../workers/webSocket.worker.ts', import.meta.url), {
type: 'module'
})
// 创建 timer worker
const timerWorker: Worker = new Worker(new URL('../workers/timer.worker.ts', import.meta.url), {
type: 'module'
})
// 添加一个标识是否是主窗口的变量
let isMainWindow = false
// LRU缓存实现
class LRUCache<K, V> {
private maxSize: number
private cache = new Map<K, V>()
constructor(maxSize: number = 1000) {
this.maxSize = maxSize
}
set(key: K, value: V) {
if (this.cache.has(key)) {
this.cache.delete(key)
} else if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value
if (firstKey) {
this.cache.delete(firstKey)
}
}
this.cache.set(key, value)
}
has(key: K): boolean {
return this.cache.has(key)
}
clear() {
this.cache.clear()
}
get size(): number {
return this.cache.size
}
}
class WS {
// 添加消息队列大小限制
readonly #MAX_QUEUE_SIZE = 50 // 减少队列大小
#tasks: WsReqMsgContentType[] = []
// 重连🔐
#connectReady = false
// 使用LRU缓存替代简单的Set
#processedMsgCache = new LRUCache<number, number>(1000) // 使用LRU缓存
#tauriListener: ReturnType<typeof useTauriListener> | null = null
// 存储连接健康状态信息
#connectionHealth = {
isHealthy: true,
lastPongTime: null as number | null,
timeSinceLastPong: null as number | null
}
// 网络重连工具,延迟初始化
#networkReconnect: ReturnType<typeof UseNetworkReconnectType> | null = null
// 存储watch清理函数
#unwatchFunctions: (() => void)[] = []
constructor() {
info('[ws] webSocket 服务初始化')
this.initWindowType()
if (isMainWindow) {
// 收到WebSocket worker消息
worker.addEventListener('message', this.onWorkerMsg)
// 收到Timer worker消息
timerWorker.addEventListener('message', this.onTimerWorkerMsg)
// 添加页面可见性监听
this.initVisibilityListener()
this.initNetworkReconnect()
}
}
// 初始化网络重连工具
private initNetworkReconnect() {
// 动态导入以延迟执行
import('@/hooks/useNetworkReconnect')
.then(({ useNetworkReconnect }) => {
this.#networkReconnect = useNetworkReconnect()
console.log('[WebSocket] 网络重连工具初始化完成')
// 监听网络在线状态变化
if (this.#networkReconnect.isOnline) {
const unwatch = watch(this.#networkReconnect.isOnline, (newValue, oldValue) => {
// 只在网络从离线变为在线时执行重连
if (newValue === true && oldValue === false) {
console.log('[WebSocket] 网络恢复在线状态主动重新初始化WebSocket连接')
// 重置重连计数并重新初始化连接
this.forceReconnect()
}
})
// 存储清理函数
this.#unwatchFunctions = this.#unwatchFunctions || []
this.#unwatchFunctions.push(unwatch)
}
})
.catch((err) => {
console.error('[WebSocket] 网络重连工具初始化失败:', err)
})
}
// 初始化页面可见性监听
private async initVisibilityListener() {
const handleVisibilityChange = (isVisible: boolean) => {
worker.postMessage(
JSON.stringify({
type: 'visibilityChange',
value: { isHidden: !isVisible }
})
)
// 优化的可见性恢复检查
if (isVisible && this.#networkReconnect?.isOnline?.value) {
// 检查最后一次通信时间,如果太久没有通信,刷新数据
const now = Date.now()
const lastPongTime = this.#connectionHealth.lastPongTime
const heartbeatTimeout = 90000 // 增加到90秒减少误触发
if (lastPongTime && now - lastPongTime > heartbeatTimeout) {
console.log('[Network] 应用从后台恢复且长时间无心跳,刷新数据')
this.#networkReconnect?.refreshAllData()
}
}
}
const debouncedVisibilityChange = useDebounceFn((isVisible: boolean) => {
handleVisibilityChange(isVisible)
}, 300)
// 使用document.visibilitychange事件 兼容web
document.addEventListener('visibilitychange', () => {
const isVisible = !document.hidden
console.log(`document visibility change: ${document.hidden ? '隐藏' : '可见'}`)
debouncedVisibilityChange(isVisible)
})
// 跟踪当前窗口状态,避免无变化时重复触发
let currentVisibilityState = true
// 创建状态变更处理器
const createStateChangeHandler = (newState: boolean) => {
return () => {
if (currentVisibilityState !== newState) {
currentVisibilityState = newState
debouncedVisibilityChange(newState)
}
}
}
try {
info('[ws] 创建Tauri窗口事件监听')
// 设置各种Tauri窗口事件监听器
// 窗口失去焦点 - 隐藏状态
addListener(listen('tauri://blur', createStateChangeHandler(false)), 'tauri://blur')
// 窗口获得焦点 - 可见状态
addListener(listen('tauri://focus', createStateChangeHandler(true)), 'tauri://focus')
// 窗口最小化 - 隐藏状态
addListener(listen('tauri://window-minimized', createStateChangeHandler(false)), 'tauri://window-minimized')
// 窗口恢复 - 可见状态
addListener(listen('tauri://window-restored', createStateChangeHandler(true)), 'tauri://window-restored')
// 窗口隐藏 - 隐藏状态
addListener(listen('tauri://window-hidden', createStateChangeHandler(false)), 'tauri://window-hidden')
// 窗口显示 - 可见状态
addListener(listen('tauri://window-shown', createStateChangeHandler(true)), 'tauri://window-shown')
} catch (error) {
console.error('无法设置Tauri Window事件监听:', error)
}
}
// 处理Timer worker消息
onTimerWorkerMsg = (e: MessageEvent<any>) => {
const data = e.data
switch (data.type) {
case 'timeout': {
// 检查是否是心跳超时消息
if (data.msgId && data.msgId.startsWith('heartbeat_timeout_')) {
// 转发给WebSocket worker
worker.postMessage(JSON.stringify({ type: 'heartbeatTimeout' }))
}
// 处理任务队列定时器超时
else if (data.msgId === 'process_tasks_timer') {
const userStore = useUserStore()
if (userStore.isSign) {
// 处理堆积的任务
for (const task of this.#tasks) {
this.send(task)
}
// 清空缓存的消息
this.#tasks = []
}
}
break
}
case 'periodicHeartbeat': {
// 心跳触发转发给WebSocket worker
worker.postMessage(JSON.stringify({ type: 'heartbeatTimerTick' }))
break
}
case 'reconnectTimeout': {
// timer上报重连超时事件转发给WebSocket worker
worker.postMessage(
JSON.stringify({
type: 'reconnectTimeout',
value: { reconnectCount: data.reconnectCount }
})
)
break
}
}
}
// 初始化窗口类型
private async initWindowType() {
const currentWindow = WebviewWindow.getCurrent()
isMainWindow = currentWindow.label === 'home' || currentWindow.label === 'rtcCall'
}
initConnect = async () => {
const token = localStorage.getItem('TOKEN')
// 如果token 是 null, 而且 localStorage 的用户信息有值,需要清空用户信息
if (token === null && localStorage.getItem('user')) {
localStorage.removeItem('user')
}
const clientId = await getEnhancedFingerprint()
const savedProxy = localStorage.getItem('proxySettings')
let serverUrl = import.meta.env.VITE_WEBSOCKET_URL
if (savedProxy) {
const settings = JSON.parse(savedProxy)
const suffix = settings.wsIp + ':' + settings.wsPort + URLEnum.WEBSOCKET + '/' + settings.wsSuffix
if (settings.wsType === 'ws' || settings.wsType === 'wss') {
serverUrl = settings.wsType + '://' + suffix
}
}
// 初始化 ws
worker.postMessage(
`{"type":"initWS","value":{"token":${token ? `"${token}"` : null},"clientId":${clientId ? `"${clientId}", "serverUrl":"${serverUrl}"` : null}}}`
)
}
onWorkerMsg = async (e: MessageEvent<any>) => {
const params: { type: string; value: unknown } = JSON.parse(e.data)
switch (params.type) {
case WorkerMsgEnum.MESSAGE: {
await this.onMessage(params.value as string)
break
}
case WorkerMsgEnum.OPEN: {
this.#dealTasks()
break
}
case WorkerMsgEnum.CLOSE:
case WorkerMsgEnum.ERROR: {
this.#onClose()
break
}
case WorkerMsgEnum.WS_ERROR: {
console.log('WebSocket错误:', (params.value as { msg: string }).msg)
useMitt.emit(WsResponseMessageType.NO_INTERNET, params.value)
// 如果是重连失败,可以提示用户刷新页面
if ((params.value as { msg: string }).msg.includes('连接失败次数过多')) {
// 可以触发UI提示让用户刷新页面
useMitt.emit('wsReconnectFailed', params.value)
}
break
}
case 'startReconnectTimer': {
console.log('worker上报心跳超时事件', params.value)
// 向timer发送startReconnectTimer事件
timerWorker.postMessage({
type: 'startReconnectTimer',
reconnectCount: (params.value as any).reconnectCount as number,
value: { delay: 1000 }
})
break
}
// 心跳定时器相关消息处理
case 'startHeartbeatTimer': {
// 启动心跳定时器
const { interval } = params.value as { interval: number }
timerWorker.postMessage({
type: 'startPeriodicHeartbeat',
interval
})
break
}
case 'stopHeartbeatTimer': {
// 停止心跳定时器
timerWorker.postMessage({
type: 'stopPeriodicHeartbeat'
})
break
}
case 'startHeartbeatTimeoutTimer': {
// 启动心跳超时定时器
const { timerId, timeout } = params.value as { timerId: string; timeout: number }
timerWorker.postMessage({
type: 'startTimer',
msgId: timerId,
duration: timeout
})
break
}
case 'clearHeartbeatTimeoutTimer': {
// 清除心跳超时定时器
const { timerId } = params.value as { timerId: string }
timerWorker.postMessage({
type: 'clearTimer',
msgId: timerId
})
break
}
case 'connectionStateChange': {
const { state, isReconnection } = params.value as { state: ConnectionState; isReconnection: boolean }
// 检测重连成功
if (isReconnection && state === ConnectionState.CONNECTED) {
console.log('🔄 WebSocket 重连成功')
// 网络重连成功后刷新数据
if (isMainWindow && this.#networkReconnect) {
console.log('开始刷新数据...')
this.#networkReconnect.refreshAllData()
} else if (isMainWindow) {
// 如果还没初始化,延迟初始化后再刷新
this.initNetworkReconnect()
}
} else if (!isReconnection && state === ConnectionState.CONNECTED) {
console.log('✅ WebSocket 首次连接成功')
}
break
}
// 处理心跳响应
case 'pongReceived': {
const { timestamp } = params.value as { timestamp: number }
this.#connectionHealth.lastPongTime = timestamp
break
}
// 处理连接健康状态
case 'connectionHealthStatus': {
const { isHealthy, lastPongTime, timeSinceLastPong } = params.value as {
isHealthy: boolean
lastPongTime: number | null
timeSinceLastPong: number | null
}
this.#connectionHealth = { isHealthy, lastPongTime, timeSinceLastPong }
useMitt.emit('wsConnectionHealthChange', this.#connectionHealth)
break
}
}
}
// 重置一些属性
#onClose = () => {
this.#connectReady = false
}
#dealTasks = () => {
this.#connectReady = true
// 先探测登录态
// this.#detectionLoginStatus()
timerWorker.postMessage({
type: 'startTimer',
msgId: 'process_tasks_timer',
duration: 500
})
}
#send(msg: WsReqMsgContentType) {
worker.postMessage(`{"type":"message","value":${typeof msg === 'string' ? msg : JSON.stringify(msg)}}`)
}
send = (params: WsReqMsgContentType) => {
if (isMainWindow) {
// 主窗口直接发送消息
if (this.#connectReady) {
this.#send(params)
} else {
// 优化的队列管理
if (this.#tasks.length >= this.#MAX_QUEUE_SIZE) {
// 优先丢弃非关键消息
const nonCriticalIndex = this.#tasks.findIndex(
(task) => typeof task === 'object' && task.type !== 1 && task.type !== 2
)
if (nonCriticalIndex !== -1) {
this.#tasks.splice(nonCriticalIndex, 1)
console.warn('消息队列已满,丢弃非关键消息')
} else {
this.#tasks.shift()
console.warn('消息队列已满,丢弃最旧消息')
}
}
this.#tasks.push(params)
}
}
}
// 收到消息回调
onMessage = async (value: string) => {
try {
const params: { type: WsResponseMessageType; data: unknown } = JSON.parse(value)
switch (params.type) {
// 获取登录二维码
case WsResponseMessageType.LOGIN_QR_CODE: {
console.log('获取二维码')
useMitt.emit(WsResponseMessageType.LOGIN_QR_CODE, params.data as LoginInitResType)
break
}
// 等待授权
case WsResponseMessageType.WAITING_AUTHORIZE: {
console.log('等待授权')
useMitt.emit(WsResponseMessageType.WAITING_AUTHORIZE)
break
}
// 登录成功
case WsResponseMessageType.LOGIN_SUCCESS: {
console.log('登录成功')
useMitt.emit(WsResponseMessageType.LOGIN_SUCCESS, params.data as LoginSuccessResType)
break
}
// 收到消息
case WsResponseMessageType.RECEIVE_MESSAGE: {
const message = params.data as MessageType
info(`[ws]收到消息: ${JSON.stringify(message)}`)
useMitt.emit(WsResponseMessageType.RECEIVE_MESSAGE, message)
break
}
// 用户状态改变
case WsResponseMessageType.USER_STATE_CHANGE: {
console.log('用户状态改变', params.data)
useMitt.emit(WsResponseMessageType.USER_STATE_CHANGE, params.data as UserStateType)
break
}
// 用户上线
case WsResponseMessageType.ONLINE: {
console.log('上线', params.data)
useMitt.emit(WsResponseMessageType.ONLINE, params.data as OnStatusChangeType)
break
}
// 用户下线
case WsResponseMessageType.OFFLINE: {
console.log('下线')
useMitt.emit(WsResponseMessageType.OFFLINE)
break
}
// 用户 token 过期
case WsResponseMessageType.TOKEN_EXPIRED: {
console.log('账号在其他设备登录')
useMitt.emit(WsResponseMessageType.TOKEN_EXPIRED, params.data as WsTokenExpire)
break
}
// 拉黑的用户的发言在禁用后,要删除他的发言
case WsResponseMessageType.INVALID_USER: {
console.log('无效用户')
useMitt.emit(WsResponseMessageType.INVALID_USER, params.data as { uid: number })
break
}
// 点赞、不满消息通知
case WsResponseMessageType.MSG_MARK_ITEM: {
useMitt.emit(WsResponseMessageType.MSG_MARK_ITEM, params.data as { markList: MarkItemType[] })
break
}
// 消息撤回通知
case WsResponseMessageType.MSG_RECALL: {
console.log('撤回')
useMitt.emit(WsResponseMessageType.MSG_RECALL, params.data as { data: RevokedMsgType })
break
}
// 新好友申请
case WsResponseMessageType.REQUEST_NEW_FRIEND: {
console.log('好友申请')
useMitt.emit(WsResponseMessageType.REQUEST_NEW_FRIEND, params.data as { uid: number; unreadCount: number })
break
}
// 成员变动
case WsResponseMessageType.NEW_FRIEND_SESSION: {
console.log('成员变动')
useMitt.emit(
WsResponseMessageType.NEW_FRIEND_SESSION,
params.data as {
roomId: number
uid: number
changeType: ChangeTypeEnum
activeStatus: OnlineEnum
lastOptTime: number
}
)
break
}
// 同意好友请求
case WsResponseMessageType.REQUEST_APPROVAL_FRIEND: {
console.log('同意好友申请', params.data)
useMitt.emit(
WsResponseMessageType.REQUEST_APPROVAL_FRIEND,
params.data as {
uid: number
}
)
break
}
// 自己修改我在群里的信息
case WsResponseMessageType.MY_ROOM_INFO_CHANGE: {
console.log('自己修改我在群里的信息', params.data)
useMitt.emit(
WsResponseMessageType.MY_ROOM_INFO_CHANGE,
params.data as {
myName: string
roomId: string
uid: string
}
)
break
}
case WsResponseMessageType.ROOM_INFO_CHANGE: {
console.log('群主修改群聊信息', params.data)
useMitt.emit(
WsResponseMessageType.ROOM_INFO_CHANGE,
params.data as {
roomId: string
name: string
avatar: string
}
)
break
}
case WsResponseMessageType.ROOM_GROUP_NOTICE_MSG: {
console.log('发布群公告', params.data)
useMitt.emit(
WsResponseMessageType.ROOM_GROUP_NOTICE_MSG,
params.data as {
id: string
content: string
top: string
}
)
break
}
case WsResponseMessageType.ROOM_EDIT_GROUP_NOTICE_MSG: {
console.log('编辑群公告', params.data)
useMitt.emit(
WsResponseMessageType.ROOM_EDIT_GROUP_NOTICE_MSG,
params.data as {
id: string
content: string
top: string
}
)
break
}
case WsResponseMessageType.ROOM_DISSOLUTION: {
console.log('群解散', params.data)
useMitt.emit(WsResponseMessageType.ROOM_DISSOLUTION, params.data)
break
}
case WsResponseMessageType.VideoCallRequest: {
const data = params.data as VideoCallRequestData
console.log('收到通话请求', data)
useMitt.emit(WsResponseMessageType.VideoCallRequest, data)
break
}
case WsResponseMessageType.CallAccepted: {
const data = params.data as CallResponseData
console.log('通话被接受', data)
useMitt.emit(WsResponseMessageType.CallAccepted, data)
break
}
case WsResponseMessageType.CallRejected: {
const data = params.data as CallResponseData
console.log('通话被拒绝', data)
useMitt.emit(WsResponseMessageType.CallRejected, data)
break
}
case WsResponseMessageType.TIMEOUT: {
const data = params.data as CallResponseData
console.log('通话超时未接通', data)
useMitt.emit(WsResponseMessageType.TIMEOUT, data)
break
}
case WsResponseMessageType.RoomClosed: {
const data = params.data as { roomId: string }
console.log('房间已关闭', data)
useMitt.emit(WsResponseMessageType.RoomClosed, data)
break
}
case WsResponseMessageType.WEBRTC_SIGNAL: {
const data = params.data as SignalData
console.log('收到信令消息', data)
useMitt.emit(WsResponseMessageType.WEBRTC_SIGNAL, data)
break
}
case WsResponseMessageType.JoinVideo: {
const data = params.data as RoomActionData
console.log('用户加入房间', data)
useMitt.emit(WsResponseMessageType.JoinVideo, data)
break
}
case WsResponseMessageType.LeaveVideo: {
const data = params.data as RoomActionData
console.log('用户离开房间', data)
useMitt.emit(WsResponseMessageType.LeaveVideo, data)
break
}
case WsResponseMessageType.DROPPED: {
const data = params.data
console.log('用户已挂断', data)
useMitt.emit(WsResponseMessageType.DROPPED, data)
break
}
default: {
console.log('接收到未处理类型的消息:', params)
break
}
}
} catch (error) {
console.error('Failed to parse WebSocket message:', error)
// 可以添加错误上报逻辑
return
}
}
// 检查连接健康状态
checkConnectionHealth() {
if (isMainWindow) {
worker.postMessage(
JSON.stringify({
type: 'checkConnectionHealth'
})
)
return this.#connectionHealth
}
return null
}
// 获取当前连接健康状态
getConnectionHealth() {
return this.#connectionHealth
}
// 强制重新连接WebSocket
forceReconnect() {
console.log('[WebSocket] 强制重新初始化WebSocket连接')
// 停止当前的重连计时器
worker.postMessage(JSON.stringify({ type: 'clearReconnectTimer' }))
// 停止心跳
worker.postMessage(JSON.stringify({ type: 'stopHeartbeat' }))
// 重置重连计数并重新初始化
worker.postMessage(JSON.stringify({ type: 'resetReconnectCount' }))
// 重新初始化连接
this.initConnect()
}
destroy() {
try {
// 优化的资源清理顺序
worker.postMessage(JSON.stringify({ type: 'clearReconnectTimer' }))
worker.postMessage(JSON.stringify({ type: 'stopHeartbeat' }))
// 同时终止timer worker相关的心跳
timerWorker.postMessage({
type: 'stopPeriodicHeartbeat'
})
// 清理内存
this.#tasks.length = 0 // 更高效的数组清空
this.#processedMsgCache.clear()
this.#connectReady = false
// 重置连接健康状态
this.#connectionHealth = {
isHealthy: true,
lastPongTime: null,
timeSinceLastPong: null
}
// 清理 Tauri 事件监听器
this.#tauriListener?.cleanup()
this.#tauriListener = null
// 清理所有watch
this.#unwatchFunctions.forEach((unwatch) => {
try {
unwatch()
} catch (error) {
console.warn('清理watch函数时出错:', error)
}
})
this.#unwatchFunctions.length = 0
// 最后终止workers
setTimeout(() => {
worker.terminate()
timerWorker.terminate()
}, 100) // 给一点时间让消息处理完成
} catch (error) {
console.error('销毁WebSocket时出错:', error)
}
}
}
const ws = new WS()
await ws.initConnect()
export default ws

View File

@@ -6,28 +6,10 @@
*/
import { info } from '@tauri-apps/plugin-log'
import { type } from '@tauri-apps/plugin-os'
// 根据平台和环境变量决定使用哪种实现
const USE_RUST_WEBSOCKET =
// 生产环境默认使用 Rust 实现
import.meta.env.PROD ||
// 或者通过环境变量强制启用
import.meta.env.VITE_USE_RUST_WEBSOCKET === 'true' ||
// 移动端强制使用 Rust 实现(性能更好)
['android', 'ios'].includes(type())
let webSocketService: any
if (USE_RUST_WEBSOCKET) {
// 使用 Rust WebSocket 实现
// TODO: 这里会初始化多次,会根据窗口来初始化,需要实现单例模式
info('🦀 使用 Rust WebSocket 实现')
webSocketService = import('./webSocketRust').then((module) => module.default)
} else {
// 使用原始的 JavaScript Worker 实现
info('🌐 使用 JavaScript WebSocket Worker 实现')
webSocketService = import('./webSocket').then((module) => module.default)
}
info('🦀 使用 Rust WebSocket 实现')
webSocketService = import('./webSocketRust').then((module) => module.default)
export default webSocketService

View File

@@ -6,9 +6,6 @@ import { WorkerMsgEnum } from '@/enums'
import { useMitt } from '@/hooks/useMitt'
import { getEnhancedFingerprint } from '@/services/fingerprint'
import { WsResponseMessageType } from '@/services/wsType'
import { useContactStore } from '@/stores/contacts'
const contactStore = useContactStore()
/// WebSocket 连接状态
export enum ConnectionState {
@@ -370,15 +367,6 @@ class RustWebSocketClient {
})
)
// 群组相关事件
this.listenerController.add(
await listen('ws-join-group', async (event: any) => {
info(`[ws]加入群组: ${JSON.stringify(event.payload)}`)
// 更新群聊列表
await contactStore.getGroupChatList()
})
)
this.listenerController.add(
await listen('ws-msg-recall', (event: any) => {
info('撤回')
@@ -417,7 +405,7 @@ class RustWebSocketClient {
// 好友相关事件
this.listenerController.add(
await listen('ws-request-new-friend', (event: any) => {
await listen('ws-request-new-apply', (event: any) => {
info('好友申请')
useMitt.emit(WsResponseMessageType.REQUEST_NEW_FRIEND, event.payload)
})

View File

@@ -19,7 +19,7 @@ export const useContactStore = defineStore(StoresEnum.CONTACTS, () => {
/** 联系人列表分页选项 */
const contactsOptions = ref({ isLast: false, isLoading: false, cursor: '' })
/** 好友请求列表分页选项 */
const requestFriendsOptions = ref({ isLast: false, isLoading: false, cursor: '', pageNo: 1 })
const applyPageOptions = ref({ isLast: false, isLoading: false, cursor: '', pageNo: 1 })
/** 群聊列表 */
const groupChatList = ref<GroupListReq[]>([])
@@ -78,37 +78,38 @@ export const useContactStore = defineStore(StoresEnum.CONTACTS, () => {
* 获取好友申请未读数
* 更新全局store中的未读计数
*/
const getNewFriendCount = async () => {
const res = await apis.newFriendCount()
const getApplyUnReadCount = async () => {
const res = await apis.applyUnReadCount()
if (!res) return
// 更新全局store中的未读计数
globalStore.unReadMark.newFriendUnreadCount = res.unReadCount
globalStore.unReadMark.newFriendUnreadCount = res.unReadCount4Friend
globalStore.unReadMark.newGroupUnreadCount = res.unReadCount4Group
}
/**
* 获取好友申请列表
* @param isFresh 是否刷新列表true则重新加载false则加载更多
*/
const getRequestFriendsList = async (isFresh = false) => {
const getApplyPage = async (isFresh = false) => {
// 非刷新模式下,如果已经加载完或正在加载中,则直接返回
if (!isFresh) {
if (requestFriendsOptions.value.isLast || requestFriendsOptions.value.isLoading) return
if (applyPageOptions.value.isLast || applyPageOptions.value.isLoading) return
}
// 设置加载状态
requestFriendsOptions.value.isLoading = true
applyPageOptions.value.isLoading = true
// 刷新时重置页码
if (isFresh) {
requestFriendsOptions.value.pageNo = 1
requestFriendsOptions.value.cursor = ''
applyPageOptions.value.pageNo = 1
applyPageOptions.value.cursor = ''
}
try {
const res = await apis.requestFriendList({
pageNo: requestFriendsOptions.value.pageNo,
const res = await apis.getApplyPage({
pageNo: applyPageOptions.value.pageNo,
pageSize: 30,
cursor: isFresh ? '' : requestFriendsOptions.value.cursor
cursor: isFresh ? '' : applyPageOptions.value.cursor
})
if (!res) return
// 刷新模式下替换整个列表,否则追加到列表末尾
@@ -119,19 +120,19 @@ export const useContactStore = defineStore(StoresEnum.CONTACTS, () => {
}
// 更新分页信息
requestFriendsOptions.value.cursor = res.cursor
requestFriendsOptions.value.isLast = res.isLast
applyPageOptions.value.cursor = res.cursor
applyPageOptions.value.isLast = res.isLast
// 如果有返回pageNo则使用服务器返回的pageNo否则自增页码
if (res.pageNo) {
requestFriendsOptions.value.pageNo = res.pageNo + 1
applyPageOptions.value.pageNo = res.pageNo + 1
} else {
requestFriendsOptions.value.pageNo++
applyPageOptions.value.pageNo++
}
} catch (error) {
console.error('获取好友申请列表失败:', error)
} finally {
requestFriendsOptions.value.isLoading = false
applyPageOptions.value.isLoading = false
}
}
@@ -144,11 +145,11 @@ export const useContactStore = defineStore(StoresEnum.CONTACTS, () => {
// 同意好友申请
apis.handleInviteApi(apply).then(async () => {
// 刷新好友申请列表
await getRequestFriendsList(true)
await getApplyPage(true)
// 刷新好友列表
await getContactList(true)
// 获取最新的未读数
await getNewFriendCount()
await getApplyUnReadCount()
// 更新当前选中联系人的状态
if (globalStore.currentSelectedContact) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -156,7 +157,7 @@ export const useContactStore = defineStore(StoresEnum.CONTACTS, () => {
globalStore.currentSelectedContact.status = RequestFriendAgreeStatus.Agree
}
// 获取最新的未读数
await getNewFriendCount()
await getApplyUnReadCount()
})
}
@@ -180,13 +181,13 @@ export const useContactStore = defineStore(StoresEnum.CONTACTS, () => {
return {
getContactList,
getGroupChatList,
getRequestFriendsList,
getNewFriendCount,
getApplyPage,
getApplyUnReadCount,
contactsList,
groupChatList,
requestFriendsList,
contactsOptions,
requestFriendsOptions,
applyPageOptions,
onDeleteContact,
onHandleInvite
}

View File

@@ -14,8 +14,13 @@ export const useGlobalStore = defineStore(
const chatStore = useChatStore()
// 未读消息标记:好友请求未读数和新消息未读数
const unReadMark = reactive<{ newFriendUnreadCount: number; newMsgUnreadCount: number }>({
const unReadMark = reactive<{
newFriendUnreadCount: number
newMsgUnreadCount: number
newGroupUnreadCount: number
}>({
newFriendUnreadCount: 0,
newGroupUnreadCount: 0,
newMsgUnreadCount: 0
})

View File

@@ -7,10 +7,7 @@
<div class="text-(14px [--text-color])">好友通知</div>
<n-flex align="center" :size="4">
<n-badge :value="globalStore.unReadMark.newFriendUnreadCount" :max="15" />
<n-badge
v-if="hasPendingFriendRequests && globalStore.unReadMark.newFriendUnreadCount === 0"
dot
color="#d5304f" />
<!-- <n-badge v-if="globalStore.unReadMark.newFriendUnreadCount > 0" dot color="#d5304f" /> -->
<svg class="size-16px rotate-270 color-[--text-color]"><use href="#down"></use></svg>
</n-flex>
</n-flex>
@@ -22,11 +19,8 @@
class="my-10px p-12px hover:(bg-[--list-hover-color] cursor-pointer)">
<div class="text-(14px [--text-color])">群通知</div>
<n-flex align="center" :size="4">
<n-badge :value="globalStore.unReadMark.newFriendUnreadCount" :max="15" />
<n-badge
v-if="hasPendingFriendRequests && globalStore.unReadMark.newFriendUnreadCount === 0"
dot
color="#d5304f" />
<n-badge :value="globalStore.unReadMark.newGroupUnreadCount" :max="15" />
<!-- <n-badge v-if="globalStore.unReadMark.newGroupUnreadCount === 0" dot color="#d5304f" /> -->
<svg class="size-16px rotate-270 color-[--text-color]"><use href="#down"></use></svg>
</n-flex>
</n-flex>
@@ -119,17 +113,18 @@
</template>
<script setup lang="ts" name="friendsList">
import { storeToRefs } from 'pinia'
import { useRoute } from 'vue-router'
import { MittEnum, OnlineEnum, RoomTypeEnum, ThemeEnum } from '@/enums'
import { useUserInfo } from '@/hooks/useCached.ts'
import { useMitt } from '@/hooks/useMitt.ts'
import { type DetailsContent, RequestFriendAgreeStatus } from '@/services/types'
import type { DetailsContent } from '@/services/types'
import { useContactStore } from '@/stores/contacts.ts'
import { useGlobalStore } from '@/stores/global.ts'
import { useSettingStore } from '@/stores/setting'
import { useUserStore } from '@/stores/user'
import { useUserStatusStore } from '@/stores/userStatus'
import { AvatarUtils } from '@/utils/AvatarUtils'
const route = useRoute()
const menuList = ref([
{ label: '添加分组', icon: 'plus' },
{ label: '重命名该组', icon: 'edit' },
@@ -142,18 +137,10 @@ const shrinkStatus = ref(false)
const contactStore = useContactStore()
const globalStore = useGlobalStore()
const userStatusStore = useUserStatusStore()
const userStore = useUserStore()
const settingStore = useSettingStore()
const { themes } = storeToRefs(settingStore)
const { stateList } = storeToRefs(userStatusStore)
/** 是否有待处理的好友申请 */
const hasPendingFriendRequests = computed(() => {
return contactStore.requestFriendsList.some(
(item) => item.status === RequestFriendAgreeStatus.Waiting && item.uid !== userStore.userInfo.uid
)
})
/** 群聊列表 */
const groupChatList = computed(() => {
console.log(contactStore.groupChatList)
@@ -205,7 +192,17 @@ const handleSelect = (event: MouseEvent) => {
console.log(event)
}
const handleApply = (applyType: 'friend' | 'group') => {
const handleApply = async (applyType: 'friend' | 'group') => {
// 刷新好友申请列表
await contactStore.getApplyPage(true)
// 更新未读数
if (applyType === 'friend') {
globalStore.unReadMark.newFriendUnreadCount = 0
} else {
globalStore.unReadMark.newGroupUnreadCount = 0
}
useMitt.emit(MittEnum.APPLY_SHOW, {
context: {
type: 'apply',
@@ -215,6 +212,16 @@ const handleApply = (applyType: 'friend' | 'group') => {
activeItem.value = ''
}
/** 获取联系人数据 */
const fetchContactData = async () => {
try {
// 同时获取好友列表和群聊列表
await Promise.all([contactStore.getContactList(), contactStore.getGroupChatList()])
} catch (error) {
console.error('获取联系人数据失败:', error)
}
}
/** 获取用户状态 */
const getUserState = (uid: string) => {
const userInfo = useUserInfo(uid).value
@@ -226,6 +233,23 @@ const getUserState = (uid: string) => {
return null
}
/** 组件挂载时获取数据 */
onMounted(async () => {
await fetchContactData()
})
/** 监听路由变化,当路由变化到当前组件时重新获取数据 */
watch(
() => route.path,
async (newPath) => {
// 当路由变化且包含 FriendsList 相关路径时,重新获取数据
if (newPath.includes('friendsList')) {
await fetchContactData()
}
},
{ immediate: false }
)
onUnmounted(() => {
detailsShow.value = false
useMitt.emit(MittEnum.DETAILS_SHOW, detailsShow.value)

View File

@@ -1,564 +0,0 @@
// 发消息给主进程
import { ConnectionState, WorkerMsgEnum } from '@/enums'
const postMsg = ({ type, value }: { type: string; value?: object }) => {
self.postMessage(JSON.stringify({ type, value }))
}
// 连接状态
let connectionState = ConnectionState.DISCONNECTED
// 最后一次收到pong消息的时间
let lastPongTime: number | null = null
// 连续心跳失败计数未收到pong响应
let consecutiveHeartbeatFailures = 0
// 最大允许的连续心跳失败次数,超过这个值会触发重连
const MAX_HEARTBEAT_FAILURES = 3
// 心跳日志记录开关
let heartbeatLoggingEnabled = false
// ws instance
let connection: WebSocket
let reconnectCount = 0
// 重连锁
let lockReconnect = false
let token: null | string = null
let clientId: null | string = null
let serverUrl: null | string = null
// 标识是否曾经成功连接过,用于区分首次连接和重连
let hasEverConnected = false
// 心跳状态
let heartbeatActive = false
// 往 ws 发消息
const connectionSend = (value: object) => {
connection?.send(JSON.stringify(value))
}
// 添加心跳超时检测
let heartbeatTimeout: string | null = null
const HEARTBEAT_TIMEOUT = 15000 // 15秒超时
const HEARTBEAT_INTERVAL = 9900 // 心跳间隔
// 上次发送心跳的时间
let lastPingSent: number | null = null
// 健康检查间隔
const HEALTH_CHECK_INTERVAL = 30000 // 30秒检查一次连接健康状态
// 健康检查定时器ID
let healthCheckTimerId: string | null = null
// 发送心跳请求使用timer.worker
const sendHeartPack = () => {
// 启动健康检查定时器
startHealthCheck()
// 标记心跳活跃
heartbeatActive = true
// 请求主线程启动心跳定时器
postMsg({
type: 'startHeartbeatTimer',
value: { interval: HEARTBEAT_INTERVAL }
})
// 记录日志
logHeartbeat('心跳定时器已启动')
}
// 启动定期健康检查
const startHealthCheck = () => {
// 清除之前的健康检查定时器
if (healthCheckTimerId) {
postMsg({
type: 'clearTimer',
value: { msgId: healthCheckTimerId }
})
healthCheckTimerId = null
}
// 设置新的健康检查定时器
const timerId = `health_check_${Date.now()}`
healthCheckTimerId = timerId
postMsg({
type: 'startTimer',
value: { msgId: timerId, duration: HEALTH_CHECK_INTERVAL }
})
logHeartbeat('健康检查定时器已启动')
}
// 心跳日志记录
const logHeartbeat = (message: string, data?: any) => {
if (heartbeatLoggingEnabled) {
console.log(`[WebSocket心跳] ${message}`, data || '')
postMsg({
type: 'heartbeatLog',
value: { message, data, timestamp: Date.now() }
})
}
}
// 发送单次心跳
const sendSingleHeartbeat = () => {
// 检查WebSocket连接状态
if (connection?.readyState !== WebSocket.OPEN) {
logHeartbeat('尝试发送心跳时发现连接未打开', { readyState: connection?.readyState })
tryReconnect()
return
}
// 记录本次发送心跳时间
lastPingSent = Date.now()
// 心跳消息类型 2
try {
connectionSend({ type: 2 })
logHeartbeat('心跳已发送', { timestamp: lastPingSent })
} catch (err) {
logHeartbeat('心跳发送失败', { error: err })
// 发送失败,可能连接已经中断但状态未更新
tryReconnect()
return
}
// 优化的连接健康检测机制
if (lastPongTime !== null) {
const timeSinceLastPong = lastPingSent - lastPongTime
const healthThreshold = HEARTBEAT_INTERVAL * 2.5 // 增加容错时间
const isConnectionHealthy = timeSinceLastPong < healthThreshold
// 如果连接不健康,通知主线程
if (!isConnectionHealthy) {
consecutiveHeartbeatFailures++
// 只在关键阈值时记录日志,减少日志开销
if (consecutiveHeartbeatFailures === 1 || consecutiveHeartbeatFailures % 3 === 0) {
logHeartbeat('连接响应缓慢', {
consecutiveFailures: consecutiveHeartbeatFailures,
timeSinceLastPong
})
}
// 延迟错误通知,避免频繁触发
if (consecutiveHeartbeatFailures >= 2) {
postMsg({
type: WorkerMsgEnum.ERROR,
value: {
msg: '连接响应较慢,可能存在网络问题',
timeSinceLastPong,
consecutiveFailures: consecutiveHeartbeatFailures
}
})
}
// 连续失败次数过多,尝试重连
if (consecutiveHeartbeatFailures >= MAX_HEARTBEAT_FAILURES) {
logHeartbeat('连续心跳失败次数过多,触发重连', { consecutiveFailures: consecutiveHeartbeatFailures })
tryReconnect()
return
}
} else {
// 重置连续失败计数
if (consecutiveHeartbeatFailures > 0) {
logHeartbeat('心跳恢复正常', { previousFailures: consecutiveHeartbeatFailures })
consecutiveHeartbeatFailures = 0
}
}
}
// 清除之前的超时计时器
if (heartbeatTimeout) {
postMsg({
type: 'clearHeartbeatTimeoutTimer',
value: { timerId: heartbeatTimeout }
})
heartbeatTimeout = null
}
// 设置新的超时计时器
const timeoutId = `heartbeat_timeout_${Date.now()}`
heartbeatTimeout = timeoutId
postMsg({
type: 'startHeartbeatTimeoutTimer',
value: { timerId: timeoutId, timeout: HEARTBEAT_TIMEOUT }
})
}
// 更新连接状态
const updateConnectionState = (newState: ConnectionState, isReconnection?: boolean) => {
connectionState = newState
postMsg({
type: 'connectionStateChange',
value: {
state: connectionState,
isReconnection: isReconnection || false
}
})
}
// 清除心跳定时器
const clearHeartPackTimer = () => {
logHeartbeat('清除心跳定时器')
heartbeatActive = false
postMsg({ type: 'stopHeartbeatTimer' })
// 清除超时定时器
if (heartbeatTimeout) {
postMsg({
type: 'clearHeartbeatTimeoutTimer',
value: { timerId: heartbeatTimeout }
})
heartbeatTimeout = null
}
// 清除健康检查定时器
if (healthCheckTimerId) {
postMsg({
type: 'clearTimer',
value: { msgId: healthCheckTimerId }
})
healthCheckTimerId = null
}
}
// 主动尝试重连
const tryReconnect = () => {
logHeartbeat('触发主动重连')
// 主动关闭当前连接
connection?.close()
// 重置心跳状态
heartbeatActive = false
// 清除心跳定时器
clearHeartPackTimer()
// 触发重连流程
updateConnectionState(ConnectionState.RECONNECTING)
if (!lockReconnect) {
lockReconnect = true
// 使用短延迟立即重连
postMsg({
type: 'startReconnectTimer',
value: {
delay: 1000, // 快速重连1秒后
reconnectCount
}
})
}
}
// 优化的智能退避算法
const getBackoffDelay = (retryCount: number) => {
const baseDelay = 1000 // 基础延迟1秒
const maxDelay = 15000 // 减少最大延迟到15秒
const multiplier = Math.min(1.5, 2 - retryCount * 0.1)
const delay = Math.min(baseDelay * multiplier ** retryCount, maxDelay)
// 减少随机抖动范围
return delay + Math.random() * 500
}
const onCloseHandler = () => {
clearHeartPackTimer()
if (lockReconnect) return
updateConnectionState(ConnectionState.RECONNECTING)
lockReconnect = true
// 使用 timer worker 发起重连
postMsg({
type: 'startReconnectTimer',
value: {
delay: getBackoffDelay(reconnectCount),
reconnectCount
}
})
}
// ws 连接 error
const onConnectError = () => {
console.log('❌ WebSocket 连接错误')
if (connection?.readyState !== WebSocket.OPEN) {
postMsg({ type: WorkerMsgEnum.WS_ERROR, value: { msg: '连接失败,请检查网络或联系管理员' } })
return
}
onCloseHandler()
postMsg({ type: WorkerMsgEnum.ERROR })
}
// ws 连接 close
const onConnectClose = () => {
console.log('📡 WebSocket 连接断开')
updateConnectionState(ConnectionState.DISCONNECTED)
onCloseHandler()
postMsg({ type: WorkerMsgEnum.CLOSE })
}
// ws 连接成功
const onConnectOpen = () => {
console.log('🔌 WebSocket 连接成功')
// 重置心跳相关状态
consecutiveHeartbeatFailures = 0
lastPongTime = null
lastPingSent = null
// 判断是否为重连在设置hasEverConnected之前
const isReconnection = hasEverConnected
// 标记已经成功连接过
hasEverConnected = true
updateConnectionState(ConnectionState.CONNECTED, isReconnection)
postMsg({ type: WorkerMsgEnum.OPEN })
// 连接成功后立即发送一次心跳
sendSingleHeartbeat()
// 然后开始定期心跳
sendHeartPack()
}
// ws 连接 接收到消息
const onConnectMsg = (e: any) => {
// 检查是否是pong消息服务器响应心跳
try {
const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data
if (data && (data.type === 'pong' || data.type === 3)) {
// 3是pong的消息类型
lastPongTime = Date.now()
// 计算心跳往返时间
let roundTripTime = null
if (lastPingSent) {
roundTripTime = lastPongTime - lastPingSent
}
// 重置连续失败计数
if (consecutiveHeartbeatFailures > 0) {
logHeartbeat('收到pong响应重置连续失败计数', {
previousFailures: consecutiveHeartbeatFailures,
roundTripTime
})
consecutiveHeartbeatFailures = 0
} else {
logHeartbeat('收到pong响应', { roundTripTime })
}
// 告知主线程收到了pong
postMsg({
type: 'pongReceived',
value: {
timestamp: lastPongTime,
roundTripTime,
consecutiveFailures: consecutiveHeartbeatFailures
}
})
}
} catch (err) {
// 解析失败则当作普通消息处理
logHeartbeat('解析消息失败', { error: err })
}
// 如果收到任何消息,说明连接是有效的,更新连接状态
if (connectionState !== ConnectionState.CONNECTED) {
updateConnectionState(ConnectionState.CONNECTED)
}
// 转发消息给主线程
postMsg({ type: WorkerMsgEnum.MESSAGE, value: e.data })
}
// 初始化 ws 连接
const initConnection = () => {
console.log('🚀 开始初始化 WebSocket 连接')
updateConnectionState(ConnectionState.CONNECTING)
connection?.removeEventListener('message', onConnectMsg)
connection?.removeEventListener('open', onConnectOpen)
connection?.removeEventListener('close', onConnectClose)
connection?.removeEventListener('error', onConnectError)
// 建立链接
// 本地配置到 .env 里面修改。生产配置在 .env.production 里面
try {
connection = new WebSocket(`${serverUrl}?clientId=${clientId}${token ? `&Token=${token}` : ''}`)
} catch (_err) {
console.log('🚀 创建 WebSocket 链接失败')
postMsg({ type: WorkerMsgEnum.WS_ERROR, value: { msg: '创建 WebSocket 链接失败' } })
}
// 收到消息
connection.addEventListener('message', onConnectMsg)
// 建立链接
connection.addEventListener('open', onConnectOpen)
// 关闭连接
connection.addEventListener('close', onConnectClose)
// 连接错误
connection.addEventListener('error', onConnectError)
}
// 停止所有心跳相关活动
const stopAllHeartbeat = () => {
console.log('停止所有心跳活动')
heartbeatActive = false
clearHeartPackTimer()
}
// 重置重连状态
const resetReconnection = () => {
reconnectCount = 0
lockReconnect = false
hasEverConnected = false
console.log('重置重连计数和状态')
}
self.onmessage = (e: MessageEvent<string>) => {
const { type, value } = JSON.parse(e.data)
switch (type) {
case 'initWS': {
reconnectCount = 0
token = value['token']
clientId = value['clientId']
serverUrl = value['serverUrl']
lastPongTime = null // 重置pong时间
initConnection()
break
}
case 'message': {
if (connection?.readyState !== 1) return
connectionSend(value)
break
}
case 'reconnectTimeout': {
console.log('重试次数: ', value.reconnectCount)
reconnectCount = value.reconnectCount + 1
console.log('重连中当前clientId:', clientId, '当前token状态:', token ? '存在' : '不存在')
initConnection()
lockReconnect = false
break
}
// 心跳定时器触发
case 'heartbeatTimerTick': {
sendSingleHeartbeat()
break
}
// 心跳超时
case 'heartbeatTimeout': {
console.log('心跳超时,重连...')
connection.close()
postMsg({ type: 'heartbeatTimeout' })
break
}
// 停止心跳
case 'stopHeartbeat': {
stopAllHeartbeat()
break
}
// 重置重连计数
case 'resetReconnectCount': {
resetReconnection()
break
}
// 清除重连计时器
case 'clearReconnectTimer': {
lockReconnect = true // 锁定重连,阻止旧的重连流程
console.log('清除重连计时器')
break
}
// 页面可见性变化
case 'visibilityChange': {
const { isHidden } = value
if (isHidden) {
console.log('页面切换到后台Web Worker继续维持心跳')
// 页面在后台Web Worker继续正常工作
} else {
console.log('页面切换到前台,恢复正常心跳')
// 立即发送一次心跳
sendSingleHeartbeat()
}
break
}
// 请求检查连接健康状态
case 'checkConnectionHealth': {
const now = Date.now()
const isHealthy = lastPongTime !== null && now - lastPongTime < HEARTBEAT_INTERVAL * 2
// 连续失败次数也是健康状态的一个指标
const healthStatus = {
isHealthy,
lastPongTime,
lastPingSent,
connectionState,
heartbeatActive,
consecutiveFailures: consecutiveHeartbeatFailures,
timeSinceLastPong: lastPongTime ? now - lastPongTime : null,
readyState: connection?.readyState
}
logHeartbeat('健康检查', healthStatus)
// 如果连接不健康但状态显示已连接,尝试修复
if (!isHealthy && connection?.readyState === WebSocket.OPEN && heartbeatActive) {
logHeartbeat('健康检查发现异常,尝试恢复心跳', healthStatus)
// 立即发送一次心跳
sendSingleHeartbeat()
}
// 如果心跳应该活跃但心跳定时器未运行,重启心跳
if (connectionState === ConnectionState.CONNECTED && !heartbeatActive) {
logHeartbeat('发现心跳停止但连接正常,重启心跳', healthStatus)
sendHeartPack()
}
postMsg({
type: 'connectionHealthStatus',
value: healthStatus
})
break
}
// 健康检查定时器触发
case 'healthCheckTimeout': {
// 定期健康检查触发
const { msgId } = value
if (msgId === healthCheckTimerId) {
// 执行健康检查
const now = Date.now()
const isHealthy = lastPongTime !== null && now - lastPongTime < HEARTBEAT_INTERVAL * 2
logHeartbeat('定期健康检查', {
isHealthy,
timeSinceLastPong: lastPongTime ? now - lastPongTime : null,
heartbeatActive,
readyState: connection?.readyState
})
// 如果不健康且连接状态异常,尝试重连
if (!isHealthy && consecutiveHeartbeatFailures >= 1) {
logHeartbeat('定期健康检查发现连接异常,尝试重连')
tryReconnect()
}
// 如果心跳定时器应该在运行但实际没有运行,重启心跳
else if (connectionState === ConnectionState.CONNECTED && !heartbeatActive) {
logHeartbeat('发现心跳停止但连接正常,重启心跳')
sendHeartPack()
}
// 继续启动下一次健康检查
startHealthCheck()
}
break
}
// 控制心跳日志记录
case 'setHeartbeatLogging': {
heartbeatLoggingEnabled = !!value.enabled
logHeartbeat(`心跳日志${heartbeatLoggingEnabled ? '已开启' : '已关闭'}`)
break
}
}
}

View File

@@ -1,5 +1,5 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import { defineConfig } from 'vite'
const host = process.env.TAURI_DEV_HOST