-
Notifications
You must be signed in to change notification settings - Fork 314
优化 MessageConnect 的记忆管理及Cleanup机制 #1248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release/v1.3
Are you sure you want to change the base?
Changes from all commits
4929850
d49eaf7
bdc4a9e
a46bca6
b719ff8
2403c40
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,8 @@ | ||||||||
| import EventEmitter from "eventemitter3"; | ||||||||
| import type { Message, MessageConnect, MessageSend, RuntimeMessageSender, TMessage, TMessageCommAction } from "./types"; | ||||||||
| import { uuidv4 } from "@App/pkg/utils/uuid"; | ||||||||
|
|
||||||||
| const listenerMgr = new EventEmitter<string, any>(); // 单一管理器 | ||||||||
|
|
||||||||
| export class ExtensionMessage implements Message { | ||||||||
| constructor(private backgroundPrimary = false) {} | ||||||||
|
|
@@ -19,8 +23,6 @@ export class ExtensionMessage implements Message { | |||||||
| if (lastError) { | ||||||||
| console.error("chrome.runtime.lastError in chrome.runtime.sendMessage:", lastError); | ||||||||
| // 通信API出错不回继续对话 | ||||||||
|
||||||||
| // 通信API出错不回继续对话 | |
| // 通信API出错不回继续对话 | |
| return; |
CodFrm marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Feb 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里使用了可选链操作符 ?.,但前面已经检查了 !this.con,这是多余的。如果代码执行到这里,this.con 一定不为 null。
建议直接使用:this.con.postMessage(data);
Copilot
AI
Feb 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里存在逻辑错误。当 this.con 为 null 时记录了错误日志,但仍然继续执行 listenerMgr.addListener,这会导致后续无法接收到任何消息(因为没有 Chrome port 在监听)。
建议改为:
- 如果
this.con为 null,应该直接返回或抛出错误 - 或者在添加监听器之前检查连接状态
Copilot
AI
Feb 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里存在竞态条件问题。在 line 196 设置 isSelfDisconnected = true 后,line 197 调用 this.con?.disconnect(),然后 line 199 立即触发 cleanup。但是:
- Chrome 的
port.disconnect()会异步触发onDisconnect监听器(line 172 注册的 cleanup 函数) - Line 199 的
listenerMgr.emit('cleanup:...')会立即同步执行 - 这可能导致 cleanup 被执行两次(一次同步,一次异步),虽然有
if (this.con)保护,但这种设计容易产生混乱
建议:
- 要么只依赖 Chrome 的
onDisconnect自动清理(删除 line 199) - 要么在手动触发 cleanup 前先移除 Chrome 的监听器,避免重复清理
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bdc4a9e 推前了
eventemitter3 是同步的
假如有这个情况发生 (实际上不会因为 Chrome.runtime.Port 自己呼叫时不会触发 onDisconnected )
this.con 会先清空
保证一次
Copilot
AI
Feb 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
使用 listenerMgr.once 限制了每个 listenerId 只能有一个 onDisconnect 回调。但在 forwardMessage 函数(server.ts:282-283)中,对同一个连接调用了两次 onDisconnect:
fromConnect.onDisconnect(toCon.disconnect.bind(toCon));
toCon.onDisconnect(fromConnect.disconnect.bind(fromConnect));这意味着第二次调用会覆盖第一次的回调,导致其中一个不会被执行。这是一个严重的 bug,会破坏连接转发的清理逻辑。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
不会. EventEmitter3 的 once 不会 覆盖原有的callback
Copilot
AI
Feb 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
同样的错误:当 this.con 为 null 时记录错误但继续执行。这里应该在连接已断开的情况下直接返回或抛出错误,而不是继续注册监听器。
另外,listenerMgr.once 意味着这个回调只会被触发一次,但如果用户多次调用 onDisconnect 添加多个回调,只有最后一个会被执行(因为都注册到同一个事件键 onDisconnect:${this.listenerId} 上,且都使用 once)。这与原有的行为不一致。
原有实现允许添加多个 onDisconnect 监听器(通过 con.onDisconnect.addListener),现在的实现只支持一个。
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -273,15 +273,15 @@ export function forwardMessage( | |||
| senderTo: MessageSend, | ||||
| middleware?: ApiFunctionSync | ||||
| ) { | ||||
| const handler = (params: any, fromCon: IGetSender) => { | ||||
| const fromConnect = fromCon.getConnect(); | ||||
| const handler = async (params: any, fromCon: IGetSender): Promise<any> => { | ||||
| const fromConnect: MessageConnect | undefined = fromCon.getConnect(); | ||||
| if (fromConnect) { | ||||
| connect(senderTo, `${prefix}/${path}`, params).then((toCon: MessageConnect) => { | ||||
| fromConnect.onMessage(toCon.sendMessage.bind(toCon)); | ||||
| toCon.onMessage(fromConnect.sendMessage.bind(fromConnect)); | ||||
| fromConnect.onDisconnect(toCon.disconnect.bind(toCon)); | ||||
| toCon.onDisconnect(fromConnect.disconnect.bind(fromConnect)); | ||||
| }); | ||||
| const toCon: MessageConnect = await connect(senderTo, `${prefix}/${path}`, params); | ||||
| fromConnect.onMessage(toCon.sendMessage.bind(toCon)); | ||||
| toCon.onMessage(fromConnect.sendMessage.bind(fromConnect)); | ||||
| fromConnect.onDisconnect(toCon.disconnect.bind(toCon)); | ||||
| toCon.onDisconnect(fromConnect.disconnect.bind(fromConnect)); | ||||
| return undefined; | ||||
|
Comment on lines
+276
to
+284
|
||||
| return undefined; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -50,7 +50,7 @@ export interface MessageConnect { | |||||
| onMessage(callback: (data: TMessage) => void): void; | ||||||
| sendMessage(data: TMessage): void; | ||||||
| disconnect(): void; | ||||||
| onDisconnect(callback: () => void): void; | ||||||
| onDisconnect(callback: (isSelfDisconnected: boolean) => void): void; | ||||||
|
||||||
| onDisconnect(callback: (isSelfDisconnected: boolean) => void): void; | |
| onDisconnect(callback: (isSelfDisconnected?: boolean) => void): void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
msgConn.onDisconnect(() => { 只是它不使用
但API里有提供
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,8 @@ import type { Message, MessageConnect, MessageSend, RuntimeMessageSender, TMessa | |||||||||||||||||||||||||||||||||||
| import { uuidv4 } from "@App/pkg/utils/uuid"; | ||||||||||||||||||||||||||||||||||||
| import EventEmitter from "eventemitter3"; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const listenerMgr = new EventEmitter<string, any>(); // 单一管理器 | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
Comment on lines
+5
to
+6
|
||||||||||||||||||||||||||||||||||||
| const listenerMgr = new EventEmitter<string, any>(); // 单一管理器 | |
| // 自动清理监听器的全局管理器,避免长期累积事件键导致内存泄漏 | |
| class AutoCleanEventEmitter extends EventEmitter<string, any> { | |
| override on(event: string, fn: (...args: any[]) => void, context?: any): this { | |
| const wrapped = (...args: any[]) => { | |
| // 触发一次后立刻移除监听,避免在全局单例上无限累积 | |
| this.off(event, wrapped, context); | |
| // 保持原有回调调用语义 | |
| // 使用提供的 context,如果没有则保持 EventEmitter 默认行为 | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-call | |
| fn.apply(context ?? this, args); | |
| }; | |
| return super.on(event, wrapped, context); | |
| } | |
| } | |
| const listenerMgr = new AutoCleanEventEmitter(); // 单一管理器,自动清理监听 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这什么鬼?不需要
Copilot
AI
Feb 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
与 extension_message.ts 相同的问题:当 this.target 为 null 时记录错误但继续执行。这会导致监听器被添加但永远不会被触发,造成无声的失败。建议在连接已断开时直接返回或抛出错误。
Copilot
AI
Feb 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
与 extension_message.ts 相同的问题:
- 竞态条件:postMessage 和 cleanup emit 可能导致重复清理
- 使用
listenerMgr.once限制只能有一个 onDisconnect 回调,破坏了多次调用的能力 - 当 target 为 null 时继续执行
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这段代码引入了严重的内存泄漏问题。使用全局单例
listenerMgr来管理所有连接实例的监听器会导致以下问题:listenerMgr上注册监听器,但即使连接断开并清理,EventEmitter 本身仍然占用内存。listenerId,这些 ID 对应的事件键会永久保留在 EventEmitter 的内部映射中。handler函数捕获了this引用,而这个 handler 被添加到 Chrome 的con.onMessage监听器中(line 171),同时也在 line 173 通过listenerMgr.once注册(这个用途不明确)。建议的替代方案:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
全部都有好好清理。别閙了copilot