fix(common): 🐛 apply issue (#318)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
/// 发送待发消息
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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'))
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
/** 消息的已读未读列表 */
|
||||
|
||||
@@ -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`, // 删除好友
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user