perf(worker): 使用worker优化计时器不准确问题 (#190)

This commit is contained in:
Dawn
2025-01-17 20:40:59 +08:00
committed by GitHub
parent 837d2b33ba
commit 000fcc289b
18 changed files with 1447 additions and 899 deletions

View File

@@ -53,15 +53,25 @@ HuLa is an instant messaging system developed with Tauri, Vite 5, Vue 3, and Typ
![img_3.png](preview/img_3.png)
![img_4.png](preview/img_4.png)
<div style="padding: 28px; display: inline-block;">
<img src="preview/img_4.png" alt="img_4.png" style="border-radius: 8px; display: block;" />
</div>
![img_5.png](preview/img_5.png)
<div style="padding: 28px; display: inline-block;">
<img src="preview/img_5.png" alt="img_5.png" style="border-radius: 8px; display: block;" />
</div>
![img_6.png](preview/img_6.png)
<div style="padding: 28px; display: inline-block;">
<img src="preview/img_6.png" alt="img_6.png" style="border-radius: 8px; display: block;" />
</div>
![img_6.png](preview/img_7.png)
<div style="padding: 28px; display: inline-block;">
<img src="preview/img_7.png" alt="img_7.png" style="border-radius: 8px; display: block;" />
</div>
![img_6.png](preview/img_8.png)
<div style="padding: 28px; display: inline-block;">
<img src="preview/img_8.png" alt="img_8.png" style="border-radius: 8px; display: block;" />
</div>
## Thanks to the following contributors!
@@ -96,7 +106,9 @@ Downloading the installation package on the web page will indicate that the inst
#### 1. Open "System Settings" - "Security & Privacy", as shown in the figure, check the box: Allow apps downloaded from "Any Source" to run:
![img.png](preview/img_9.png)
<div style="padding: 28px; display: inline-block;">
<img src="preview/img_9.png" alt="img_9.png" style="border-radius: 8px; display: block;" />
</div>
#### 2. If an error is reported, run the following command in the terminal to resolve the problem:
@@ -118,6 +130,8 @@ use **pnpm run commit** to invoke the _git commit_ interaction and follow the pr
**The final interpretation of this disclaimer belongs to the developer**
## License
## HuLa社区讨论群
<img src="preview/wx.jpg" width="240" height="280" alt="微信群二维码" style="border-radius: 12px;" />
## License
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FHuLaSpark%2FHuLa.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FHuLaSpark%2FHuLa?ref=badge_large)

View File

@@ -53,15 +53,25 @@ HuLa 是一个基于 Tauri、Vite 5、Vue 3 和 TypeScript 构建的即时通讯
![img_3.png](preview/img_3.png)
![img_4.png](preview/img_4.png)
<div style="padding: 28px; display: inline-block;">
<img src="preview/img_4.png" alt="img_4.png" style="border-radius: 8px; display: block;" />
</div>
![img_5.png](preview/img_5.png)
<div style="padding: 28px; display: inline-block;">
<img src="preview/img_5.png" alt="img_5.png" style="border-radius: 8px; display: block;" />
</div>
![img_6.png](preview/img_6.png)
<div style="padding: 28px; display: inline-block;">
<img src="preview/img_6.png" alt="img_6.png" style="border-radius: 8px; display: block;" />
</div>
![img_6.png](preview/img_7.png)
<div style="padding: 28px; display: inline-block;">
<img src="preview/img_7.png" alt="img_7.png" style="border-radius: 8px; display: block;" />
</div>
![img_6.png](preview/img_8.png)
<div style="padding: 28px; display: inline-block;">
<img src="preview/img_8.png" alt="img_8.png" style="border-radius: 8px; display: block;" />
</div>
## 感谢以下贡献者们!
@@ -96,7 +106,9 @@ pnpm run tauri:build
#### 1. 打开 “系统设置” - “安全性与隐私”,如图勾选:允许 “任何来源” 下载的 App 运行:
![img.png](preview/img_9.png)
<div style="padding: 28px; display: inline-block;">
<img src="preview/img_9.png" alt="img_9.png" style="border-radius: 8px; display: block;" />
</div>
#### 2. 如果还报错,请在终端执行以下命令解决:
@@ -118,6 +130,8 @@ sudo xattr -rd com.apple.quarantine 你的安装包路径/HuLa.app
**本免责声明的最终解释权归开发者所有**
## HuLa社区讨论群
<img src="preview/wx.jpg" width="240" height="280" alt="微信群二维码" style="border-radius: 12px;" />
## License
## 许可证
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FHuLaSpark%2FHuLa.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FHuLaSpark%2FHuLa?ref=badge_large)

View File

@@ -62,20 +62,20 @@
"@tauri-apps/api": "2.2.0",
"@tauri-apps/plugin-autostart": "2.2.0",
"@tauri-apps/plugin-clipboard-manager": "2.2.0",
"@tauri-apps/plugin-fs": "~2",
"@tauri-apps/plugin-fs": "~2.2.0",
"@tauri-apps/plugin-http": "2.2.0",
"@tauri-apps/plugin-notification": "^2.2.0",
"@tauri-apps/plugin-os": "2.2.0",
"@tauri-apps/plugin-process": "2.2.0",
"@tauri-apps/plugin-shell": "^2.2.0",
"@tauri-apps/plugin-sql": "^2.0.1",
"@tauri-apps/plugin-updater": "~2",
"@tauri-apps/plugin-updater": "~2.3.1",
"colorthief": "^2.6.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.11",
"dompurify": "^3.2.3",
"grapheme-splitter": "^1.0.4",
"hula-emojis": "^1.2.3",
"hula-emojis": "^1.2.5",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"naive-ui": "^2.41.0",
@@ -90,24 +90,24 @@
"@babel/eslint-parser": "^7.25.9",
"@commitlint/cli": "^19.6.0",
"@commitlint/config-conventional": "^19.6.0",
"@release-it/bumper": "^6.0.1",
"@release-it/conventional-changelog": "9.0.4",
"@release-it/bumper": "^7.0.0",
"@release-it/conventional-changelog": "10.0.0",
"@rollup/plugin-terser": "^0.4.4",
"@tauri-apps/cli": "2.0.4",
"@types/crypto-js": "^4.2.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.14.14",
"@types/node": "^22.10.7",
"@typescript-eslint/eslint-plugin": "7.1.0",
"@typescript-eslint/parser": "^7.15.0",
"@unocss/preset-uno": "^0.65.3",
"@unocss/reset": "^0.65.3",
"@unocss/transformer-directives": "^0.65.3",
"@unocss/transformer-variant-group": "^0.65.3",
"@unocss/vite": "^0.65.3",
"@unocss/preset-uno": "^65.4.2",
"@unocss/reset": "^65.4.2",
"@unocss/transformer-directives": "^65.4.2",
"@unocss/transformer-variant-group": "^65.4.2",
"@unocss/vite": "^65.4.2",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "^2.1.8",
"@vitest/coverage-v8": "^3.0.1",
"@vitest/ui": "^3.0.1",
"@vue/test-utils": "^2.4.6",
"@vueuse/core": "^12.0.0",
"chalk": "^5.3.0",
@@ -123,13 +123,13 @@
"lint-staged": "^15.2.7",
"oxlint": "^0.2.18",
"prettier": "^3.3.2",
"release-it": "^17.11.0",
"release-it": "^18.1.0",
"sass": "1.83.0",
"typescript": "^5.7.2",
"unplugin-auto-import": "^0.18.6",
"unplugin-vue-components": "^0.27.5",
"unplugin-auto-import": "^19.0.0",
"unplugin-vue-components": "^28.0.0",
"vite": "6.0.7",
"vitest": "^2.1.8",
"vitest": "^3.0.1",
"vue-tsc": "^2.2.0"
},
"config": {

1662
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
preview/wx.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

View File

@@ -12,12 +12,12 @@ const __dirname = dirname(__filename)
* @param {string} description - 检查描述
*/
async function runScript(scriptPath, description) {
const startTime = Date.now()
const startTime = performance.now()
console.log(chalk.blue(`\n[HuLa ${new Date().toLocaleTimeString()}] 开始${description}...\n`))
try {
execSync(`node ${scriptPath}`, { stdio: 'inherit' })
const duration = ((Date.now() - startTime) / 1000).toFixed(2)
const duration = ((performance.now() - startTime) / 1000).toFixed(2)
console.log(chalk.green(`\n${description}完成 (${duration}s)\n`))
return true
} catch (error) {
@@ -41,7 +41,7 @@ async function main() {
}
]
const startTime = Date.now()
const startTime = performance.now()
for (const check of checks) {
const success = await runScript(check.script, check.description)
@@ -51,7 +51,7 @@ async function main() {
}
}
const totalDuration = ((Date.now() - startTime) / 1000).toFixed(2)
const totalDuration = ((performance.now() - startTime) / 1000).toFixed(2)
console.log(chalk.green(`\n✨ 所有检查通过!总用时:${totalDuration}s\n`))
}

33
src-tauri/Cargo.lock generated
View File

@@ -558,9 +558,9 @@ dependencies = [
[[package]]
name = "cargo_toml"
version = "0.17.2"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719"
checksum = "5fbd1fe9db3ebf71b89060adaf7b0504c2d6a425cf061313099547e382c2e472"
dependencies = [
"serde",
"toml 0.8.19",
@@ -5166,9 +5166,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
version = "2.2.0"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e2e3349fbb2be7af9fad1b43d61ac83ba55ab48d47fbe1b2732f0c8211610a9"
checksum = "2979ec5ec5a9310b15d1548db3b8de98d8f75abf2b5b00fec9cd5c0553ecc09c"
dependencies = [
"anyhow",
"bytes",
@@ -5217,9 +5217,9 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "2.0.4"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b274ec7239ada504deb615f1c8abd7ba99631e879709e6f10e5d17217058d976"
checksum = "8e950124f6779c6cf98e3260c7a6c8488a74aa6350dd54c6950fdaa349bca2df"
dependencies = [
"anyhow",
"cargo_toml",
@@ -5404,9 +5404,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-notification"
version = "2.2.0"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46ab803095f14ac6521fdb6477210a49e86fed6623c3c97d8e4b2b35e045e922"
checksum = "0f8d3ee5207d3359ca2b714545664f24f70374d795bf91f7c1935a494003a57d"
dependencies = [
"log",
"notify-rust",
@@ -5472,9 +5472,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-single-instance"
version = "2.2.0"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f36019ee9832dc99e4450bb55a21cfad8633b19c2c18bd17c7741939b070ede"
checksum = "47c387d4d96690131dc46d1d2827df5c222b896a2bfeb15a16267229a55c50b5"
dependencies = [
"serde",
"serde_json",
@@ -5482,7 +5482,7 @@ dependencies = [
"thiserror 2.0.9",
"tracing",
"windows-sys 0.59.0",
"zbus 4.4.0",
"zbus 5.2.0",
]
[[package]]
@@ -5506,9 +5506,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-updater"
version = "2.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7351014c140906bcfff59d96e04b1170c8f602557f40eb37f7de356d4e7067b"
checksum = "ce2d39224390c41ba544f02b4f1721f42256320b3fb8c371e9425cbddeb4a68c"
dependencies = [
"base64 0.22.1",
"dirs 5.0.1",
@@ -7244,8 +7244,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb67eadba43784b6fb14857eba0d8fc518686d3ee537066eb6086dc318e2c8a1"
dependencies = [
"async-broadcast",
"async-executor",
"async-fs",
"async-io",
"async-lock",
"async-process",
"async-recursion",
"async-task",
"async-trait",
"blocking",
"enumflags2",
"event-listener",
"futures-core",

View File

@@ -24,10 +24,10 @@ name = "hula_app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.0.2", features = [] }
tauri-build = { version = "2.0.5", features = [] }
[dependencies]
tauri = { version = "2.2.0", features = [
tauri = { version = "2.2.2", features = [
"macos-private-api",
"tray-icon",
"image-png",
@@ -44,10 +44,10 @@ tauri-plugin-dialog = "2.2.0"
tauri-plugin-upload = "2.2.0"
tauri-plugin-global-shortcut = "2.2.0"
tauri-plugin-clipboard-manager = "2.2.0"
tauri-plugin-updater = "2.3.0"
tauri-plugin-updater = "2.3.1"
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
tauri-plugin-single-instance = "2.2.0"
tauri-plugin-notification = "2.2.0"
tauri-plugin-single-instance = "2.2.1"
tauri-plugin-notification = "2.2.1"
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-autostart = "2.2.0"
lazy_static = "1.4"

View File

@@ -813,9 +813,6 @@ onUnmounted(() => {
}
hoverBubble.value.key = -1
window.removeEventListener('click', closeMenu, true)
// 清理所有过期定时器
chatStore.clearAllExpirationTimers()
})
</script>

View File

@@ -334,3 +334,11 @@ export const enum TriggerEnum {
AI = '/',
TOPIC = '#'
}
/** 连接状态枚举 */
export enum ConnectionState {
CONNECTING = 'connecting',
CONNECTED = 'connected',
DISCONNECTED = 'disconnected',
RECONNECTING = 'reconnecting'
}

View File

@@ -28,6 +28,7 @@ const routes: Array<RouteRecordRaw> = [
// 默认导航第一个子路由
{
path: '',
name: 'mobileMessage',
redirect: '/mobile/message'
},
{

View File

@@ -143,8 +143,6 @@ async function Http<T = any>(
async function attemptFetch(currentAttempt: number): Promise<{ data: T; resp: Response } | T> {
try {
const response = await fetch(url, fetchOptions)
console.log(`Attempt ${currentAttempt + 1}: status = ${response.status}`)
// 若响应不 OK 并且状态码属于需重试列表,则抛出 FetchRetryError
if (!response.ok) {
const errorType = getErrorType(response.status)
@@ -177,7 +175,7 @@ async function Http<T = any>(
}
return responseData
} catch (error) {
console.error(`Attempt ${currentAttempt + 1} failed`, error)
console.error(`尝试 ${currentAttempt + 1} 失败的`, error)
// 检查是否仍需重试
if (!shouldRetry(currentAttempt, retries, abort)) {

View File

@@ -6,18 +6,25 @@ import type {
OnStatusChangeType
} from '@/services/wsType.ts'
import type { MessageType, MarkItemType, RevokedMsgType } from '@/services/types'
import { OnlineEnum, ChangeTypeEnum, WorkerMsgEnum } from '@/enums'
import { worker } from '@/utils/InitWorker.ts'
import { OnlineEnum, ChangeTypeEnum, WorkerMsgEnum, ConnectionState } from '@/enums'
import { useMitt } from '@/hooks/useMitt.ts'
import { useUserStore } from '@/stores/user'
import { getEnhancedFingerprint } from '@/services/fingerprint.ts'
// 创建 webSocket worker
const worker: Worker = new Worker(new URL('../workers/webSocket.worker.ts', import.meta.url), {
type: 'module'
})
class WS {
// 添加消息队列大小限制
readonly #MAX_QUEUE_SIZE = 100
#tasks: WsReqMsgContentType[] = []
// 重连🔐
#connectReady = false
// TODO: 暂时使用去重复的逻辑,后续优化
#processedMsgIds = new Set<number>()
// 使用LRU缓存替代简单的Set
#processedMsgCache = new Map<number, number>()
readonly #MAX_CACHE_SIZE = 1000
constructor() {
this.initConnect()
@@ -54,11 +61,24 @@ class WS {
this.#onClose()
break
}
case WorkerMsgEnum.WS_ERROR:
console.log('无网络连接')
case WorkerMsgEnum.WS_ERROR: {
console.log('WebSocket错误:', (params.value as { msg: string }).msg)
useMitt.emit(WsResponseMessageType.NO_INTERNET, params.value)
localStorage.removeItem('wsLogin')
// 如果是重连失败,可以提示用户刷新页面
if ((params.value as { msg: string }).msg.includes('连接失败次数过多')) {
// 可以触发UI提示让用户刷新页面
useMitt.emit('wsReconnectFailed', params.value)
}
break
}
case 'connectionStateChange': {
const { state } = params.value as { state: ConnectionState }
// 可以触发事件通知其他组件
console.log('连接状态改变', state)
useMitt.emit('wsConnectionStateChange', state)
break
}
}
}
@@ -93,125 +113,147 @@ class WS {
if (this.#connectReady) {
this.#send(params)
} else {
// 放到队列
// 队列限制
if (this.#tasks.length >= this.#MAX_QUEUE_SIZE) {
console.warn('消息队列已满,正在丢弃最旧的消息')
this.#tasks.shift()
}
this.#tasks.push(params)
}
}
// 收到消息回调
onMessage = async (value: string) => {
// FIXME 可能需要 try catch,
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
// TODO: 暂时使用去重复的逻辑,后续优化
if (this.#isMessageProcessed(message.message.id)) {
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
}
useMitt.emit(WsResponseMessageType.RECEIVE_MESSAGE, message)
break
}
// 用户上线
case WsResponseMessageType.ONLINE: {
console.log('上线')
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('token过期')
localStorage.removeItem('TOKEN')
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: {
console.log('点赞')
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: {
// TODO: 发送申请后其他人没有接收到好友申请请求,后端查看是否有问题
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
// 等待授权
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
// TODO: 暂时使用去重复的逻辑,后续优化
if (this.#isMessageProcessed(message.message.id)) {
break
}
)
break
}
default: {
console.log('接收到未处理类型的消息:', params)
break
useMitt.emit(WsResponseMessageType.RECEIVE_MESSAGE, message)
break
}
// 用户上线
case WsResponseMessageType.ONLINE: {
console.log('上线')
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('token过期')
localStorage.removeItem('TOKEN')
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: {
console.log('点赞')
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: {
// TODO: 发送申请后其他人没有接收到好友申请请求,后端查看是否有问题
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
}
default: {
console.log('接收到未处理类型的消息:', params)
break
}
}
} catch (error) {
console.error('Failed to parse WebSocket message:', error)
// 可以添加错误上报逻辑
return
}
}
// TODO: 暂时使用去重复的逻辑,后续优化
#isMessageProcessed(msgId: number): boolean {
if (this.#processedMsgIds.has(msgId)) {
const now = Date.now()
const lastProcessed = this.#processedMsgCache.get(msgId)
if (lastProcessed && now - lastProcessed < 5000) {
return true
}
// 添加到已处理集合
this.#processedMsgIds.add(msgId)
// 清理过期缓存
if (this.#processedMsgCache.size >= this.#MAX_CACHE_SIZE) {
const oldestEntries = Array.from(this.#processedMsgCache.entries())
.sort(([, a], [, b]) => a - b)
.slice(0, Math.floor(this.#MAX_CACHE_SIZE / 2))
// 设置5秒后从集合中删除
setTimeout(() => {
this.#processedMsgIds.delete(msgId)
}, 5000)
oldestEntries.forEach(([key]) => this.#processedMsgCache.delete(key))
}
this.#processedMsgCache.set(msgId, now)
return false
}
destroy() {
worker.postMessage(JSON.stringify({ type: 'clearReconnectTimer' }))
worker.terminate()
this.#tasks = []
this.#processedMsgCache.clear()
this.#connectReady = false
}
}
export default new WS()

View File

@@ -28,6 +28,14 @@ let isFirstInit = false
// 撤回消息的过期时间
const RECALL_EXPIRATION_TIME = 2 * 60 * 1000 // 2分钟单位毫秒
// 创建src/workers/timer.worker.ts
const timerWorker = new Worker(new URL('../workers/timer.worker.ts', import.meta.url))
// 添加错误处理
timerWorker.onerror = (error) => {
console.error('[Worker Error]', error)
}
export const useChatStore = defineStore(
'chat',
() => {
@@ -59,7 +67,7 @@ export const useChatStore = defineStore(
// 存储撤回的消息内容和时间
const recalledMessages = reactive<Map<number, RecalledMessage>>(new Map())
// 存储每条撤回消息的过期定时器
const expirationTimers = new Map<number, number>()
const expirationTimers = new Map<number, boolean>()
// 当前聊天室的消息Map计算属性
const currentMessageMap = computed({
@@ -222,9 +230,10 @@ export const useChatStore = defineStore(
const getSessionList = async (isFresh = false) => {
if (!isFresh && (sessionOptions.isLast || sessionOptions.isLoading)) return
sessionOptions.isLoading = true
// TODO: 这里先请求100条会话列表后续优化
const response = await apis
.getSessionList({
pageSize: sessionList.length > pageSize ? sessionList.length : pageSize,
pageSize: sessionList.length > 100 ? sessionList.length : 100,
cursor: isFresh || !sessionOptions.cursor ? '' : sessionOptions.cursor
})
.catch(() => {
@@ -419,15 +428,15 @@ export const useChatStore = defineStore(
recallTime
})
// 为这条消息设置过期定时器 TODO: setTimeout会有精度问题
const timeoutId = window.setTimeout(() => {
recalledMessages.delete(msgId)
expirationTimers.delete(msgId)
triggerMessageMapUpdate()
}, RECALL_EXPIRATION_TIME)
// 使用 Worker 来处理定时器
timerWorker.postMessage({
type: 'startTimer',
msgId,
duration: RECALL_EXPIRATION_TIME
})
// 存储定时器ID以便清理
expirationTimers.set(msgId, timeoutId)
// 记录这个消息ID已经有了定时器
expirationTimers.set(msgId, true)
message.message.type = MsgEnum.RECALL
const cacheUser = cachedStore.userCachedList[data.recallUid]
@@ -464,14 +473,6 @@ export const useChatStore = defineStore(
return recalledMessages.get(msgId)
}
// 清理所有定时器
const clearAllExpirationTimers = () => {
expirationTimers.forEach((timerId) => {
clearTimeout(timerId)
})
expirationTimers.clear()
}
// 删除消息
const deleteMsg = (msgId: number) => {
currentMessageMap.value?.delete(msgId)
@@ -526,6 +527,39 @@ export const useChatStore = defineStore(
sessionList.splice(index, 1)
}
// 监听 Worker 消息
timerWorker.onmessage = (e) => {
const { type, msgId } = e.data
if (type === 'timeout') {
console.log(`[Timeout] 消息ID: ${msgId} 已过期`)
recalledMessages.delete(msgId)
expirationTimers.delete(msgId)
triggerMessageMapUpdate()
} else if (type === 'allTimersCompleted') {
// 所有定时器都完成了,可以安全地清理资源
clearAllExpirationTimers()
terminateWorker()
}
}
// 终止 worker
const terminateWorker = () => {
timerWorker.terminate()
}
// 清理所有定时器
const clearAllExpirationTimers = () => {
expirationTimers.forEach((_, msgId) => {
// 通知 worker 停止对应的定时器
timerWorker.postMessage({
type: 'clearTimer',
msgId
})
})
expirationTimers.clear()
}
return {
getMsgIndex,
chatMessageList,

View File

@@ -1,3 +0,0 @@
export const worker: Worker = new Worker(new URL('./Worker.ts', import.meta.url), {
type: 'module'
})

View File

@@ -71,7 +71,7 @@
class="item-box w-full h-75px mb-5px"
v-for="item in groupChatList"
:key="item.roomId">
<n-flex v-slide align="center" :size="10" class="h-75px pl-6px pr-8px flex-1 truncate">
<n-flex align="center" :size="10" class="h-75px pl-6px pr-8px flex-1 truncate">
<n-avatar
round
bordered

111
src/workers/timer.worker.ts Normal file
View File

@@ -0,0 +1,111 @@
// 修改类型定义以支持字符串和数字类型的key
type TimerId = number | string
type TimerInfo = {
timerId: NodeJS.Timeout
debugId: NodeJS.Timeout | null
}
// 存储定时器ID和调试定时器ID
const timerIds = new Map<TimerId, TimerInfo>()
// 添加一个计数器来跟踪活动的定时器数量
let activeTimers = 0
// 检查并通知所有定时器是否完成
const checkAllTimersCompleted = () => {
if (activeTimers === 0) {
self.postMessage({ type: 'allTimersCompleted' })
}
}
// 添加调试信息打印函数
const logDebugInfo = (msgId: number, remainingTime: number) => {
console.log(`[Worker Debug] 消息ID: ${msgId}, 剩余时间: ${(remainingTime / 1000).toFixed(1)}`)
self.postMessage({
type: 'debug',
msgId,
remainingTime,
timestamp: Date.now()
})
}
self.onmessage = (e) => {
const { type, msgId, duration, reconnectCount } = e.data
switch (type) {
case 'startReconnectTimer': {
const timerId = setTimeout(() => {
self.postMessage({
type: 'reconnectTimeout',
value: { reconnectCount }
})
}, e.data.value.delay)
// 现在可以使用字符串作为key了
timerIds.set('reconnect', { timerId, debugId: null })
break
}
case 'clearReconnectTimer': {
if (timerIds.has('reconnect')) {
const { timerId } = timerIds.get('reconnect')!
clearTimeout(timerId)
timerIds.delete('reconnect')
}
break
}
case 'startTimer': {
activeTimers++
// 使用数字类型的msgId
if (timerIds.has(msgId)) {
const { timerId, debugId } = timerIds.get(msgId)!
clearTimeout(timerId)
if (debugId) clearInterval(debugId)
timerIds.delete(msgId)
}
const startTime = Date.now()
// 立即打印一次初始状态
logDebugInfo(msgId, duration)
// 创建定时打印的间隔器每1000ms打印一次
const debugId = setInterval(() => {
const elapsed = Date.now() - startTime
const remaining = duration - elapsed
if (remaining > 0) {
logDebugInfo(msgId, remaining)
} else {
clearInterval(debugId)
}
}, 1000) // 每秒打印一次
const timerId = setTimeout(() => {
clearInterval(debugId)
console.log('[Worker] 定时器到期:', msgId)
self.postMessage({ type: 'timeout', msgId })
timerIds.delete(msgId)
activeTimers--
checkAllTimersCompleted()
}, duration)
timerIds.set(msgId, { timerId, debugId })
break
}
case 'clearTimer': {
console.log('[Worker] 清理定时器:', msgId)
if (timerIds.has(msgId)) {
const { timerId, debugId } = timerIds.get(msgId)!
clearTimeout(timerId)
if (debugId) clearInterval(debugId)
timerIds.delete(msgId)
activeTimers--
checkAllTimersCompleted()
}
break
}
}
}

View File

@@ -1,5 +1,5 @@
// 发消息给主进程
import { WorkerMsgEnum } from '@/enums'
import { ConnectionState, WorkerMsgEnum } from '@/enums'
const postMsg = ({ type, value }: { type: string; value?: object }) => {
self.postMessage(JSON.stringify({ type, value }))
@@ -13,11 +13,8 @@ let heartTimer: number | null = null
// 重连次数上限
const reconnectCountMax = 5
let reconnectCount = 0
// 重连 timer
let timer: null | number = null
// 重连🔐
let lockReconnect = false
// 重连🔐
let token: null | string = null
let clientId: null | string = null
@@ -27,14 +24,30 @@ const connectionSend = (value: object) => {
connection?.send(JSON.stringify(value))
}
// 添加心跳超时检测
let heartbeatTimeout: number | null = null
const HEARTBEAT_TIMEOUT = 15000 // 15秒超时
// 发送心跳 10s 内发送
const sendHeartPack = () => {
// 10s 检测心跳
heartTimer = setInterval(() => {
// 心跳消息类型 2
connectionSend({ type: 2 })
// 清除之前的超时计时器
if (heartbeatTimeout) {
clearTimeout(heartbeatTimeout)
}
// 设置新的超时计时器
heartbeatTimeout = setTimeout(() => {
console.log('心跳超时,重连...')
connection.close()
}, HEARTBEAT_TIMEOUT) as any
}, 9900) as any
}
// 清除❤️跳 timer
const clearHeartPackTimer = () => {
if (heartTimer) {
@@ -43,33 +56,38 @@ const clearHeartPackTimer = () => {
}
}
const getBackoffDelay = (retryCount: number) => {
const baseDelay = 1000 // 基础延迟1秒
const maxDelay = 30000 // 最大延迟30秒
const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay)
return delay + Math.random() * 1000 // 添加随机抖动
}
const onCloseHandler = () => {
clearHeartPackTimer()
// 已经在连接中就不重连了
if (lockReconnect) return
// 标识重连中
lockReconnect = true
// 清除 timer避免任务堆积。
if (timer) {
clearTimeout(timer)
timer = null
}
// 达到重连次数上限
// 添加重连次数限制
if (reconnectCount >= reconnectCountMax) {
reconnectCount = 0
postMsg({ type: WorkerMsgEnum.WS_ERROR, value: { msg: '连接失败,请检查网络或联系管理员' } })
console.log('达到最大重连次数,停止重连')
postMsg({
type: WorkerMsgEnum.WS_ERROR,
value: { msg: '连接失败次数过多,请刷新页面重试' }
})
return
}
// 断线重连
timer = setTimeout(async () => {
initConnection()
reconnectCount++
// 标识已经开启重连任务
lockReconnect = false
}, 2000) as any
updateConnectionState(ConnectionState.RECONNECTING)
lockReconnect = true
// 使用 timer worker 替代 setTimeout
postMsg({
type: 'startReconnectTimer',
value: {
delay: getBackoffDelay(reconnectCount),
reconnectCount
}
})
}
// ws 连接 error
@@ -83,14 +101,15 @@ const onConnectError = () => {
}
// ws 连接 close
const onConnectClose = () => {
updateConnectionState(ConnectionState.DISCONNECTED)
onCloseHandler()
token = null
postMsg({ type: WorkerMsgEnum.CLOSE })
}
// ws 连接成功
const onConnectOpen = () => {
updateConnectionState(ConnectionState.CONNECTED)
postMsg({ type: WorkerMsgEnum.OPEN })
// 心跳❤️检测
sendHeartPack()
}
// ws 连接 接收到消息
@@ -98,6 +117,7 @@ const onConnectMsg = (e: any) => postMsg({ type: WorkerMsgEnum.MESSAGE, value: e
// 初始化 ws 连接
const initConnection = () => {
updateConnectionState(ConnectionState.CONNECTING)
connection?.removeEventListener('message', onConnectMsg)
connection?.removeEventListener('open', onConnectOpen)
connection?.removeEventListener('close', onConnectClose)
@@ -119,6 +139,14 @@ const initConnection = () => {
connection.addEventListener('error', onConnectError)
}
let connectionState = ConnectionState.DISCONNECTED
// 更新连接状态
const updateConnectionState = (newState: ConnectionState) => {
connectionState = newState
postMsg({ type: 'connectionStateChange', value: { state: connectionState } })
}
self.onmessage = (e: MessageEvent<string>) => {
console.log(e.data)
const { type, value } = JSON.parse(e.data)
@@ -135,5 +163,20 @@ self.onmessage = (e: MessageEvent<string>) => {
connectionSend(value)
break
}
case 'reconnectTimeout': {
reconnectCount = value.reconnectCount + 1
// 如果没有超过最大重连次数才继续重连
if (reconnectCount < reconnectCountMax) {
initConnection()
lockReconnect = false
} else {
console.log('达到最大重连次数,停止重连')
postMsg({
type: WorkerMsgEnum.WS_ERROR,
value: { msg: '连接失败次数过多,请刷新页面重试' }
})
}
break
}
}
}