From 7528ba0116aba517b078f6f6016f46e84a1112f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 2 Oct 2026 11:35:13 +0800 Subject: [PATCH] =?UTF-8?q?unhandledrejection=E6=A1=A5=E6=8E=A5=E4=BF=9D?= =?UTF-8?q?=E7=95=99once=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 用 rawListeners 记录原始监听器,拆卸时把 once 包装按 process.once 装回,普通监听器仍按 process.on 装回 - 未被 window 消费时调用 once 包装包裹的原函数,避免脱离 emitter 直调包装 - 注释记录安装期间新增监听器排在恢复监听器之前的既有边界 --- .../tests/unhandledRejectionBridge.ts | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/apps/ai-game-creator-shell/tests/unhandledRejectionBridge.ts b/apps/ai-game-creator-shell/tests/unhandledRejectionBridge.ts index 85dc4c24e..c61cabea4 100644 --- a/apps/ai-game-creator-shell/tests/unhandledRejectionBridge.ts +++ b/apps/ai-game-creator-shell/tests/unhandledRejectionBridge.ts @@ -10,11 +10,31 @@ * 有人接手;没人读说明这条拒绝是意外的,交回安装前的监听器(含 vitest 的失败上报), * 不会被静默吞掉。 * + * 安装前用 `rawListeners` 取原始监听器:`process.once` 注册的包装(`.listener`)在拆卸时 + * 按 `once` 装回去,不会退化成每次拒绝都触发的普通监听器;安装期间新增的监听器仍排在 + * 恢复出来的监听器之前。 + * * 用法:用例内 `const uninstall = installUnhandledRejectionBridge()`,在 `finally` 或 * `afterEach` 里 `uninstall()`。 */ +type RejectionListener = (reason: unknown, promise: Promise) => void; +type RawRejectionListener = RejectionListener & { + listener?: RejectionListener; +}; + +function invokeRejectionListener( + listener: RawRejectionListener, + reason: unknown, + promise: Promise, +) { + // once 包装必须调它包裹的原函数:脱离 emitter 直接调用包装会碰到 this 为空。 + (listener.listener ?? listener)(reason, promise); +} + export function installUnhandledRejectionBridge() { - const originalListeners = process.listeners('unhandledRejection'); + const originalListeners = process.rawListeners( + 'unhandledRejection', + ) as RawRejectionListener[]; process.removeAllListeners('unhandledRejection'); const handleRejection = (reason: unknown, promise: Promise) => { let consumed = false; @@ -28,7 +48,7 @@ export function installUnhandledRejectionBridge() { window.dispatchEvent(event); if (!consumed) { for (const listener of originalListeners) { - listener(reason, promise); + invokeRejectionListener(listener, reason, promise); } } }; @@ -36,7 +56,11 @@ export function installUnhandledRejectionBridge() { return () => { process.removeListener('unhandledRejection', handleRejection); for (const listener of originalListeners) { - process.on('unhandledRejection', listener); + if (listener.listener) { + process.once('unhandledRejection', listener.listener); + } else { + process.on('unhandledRejection', listener); + } } }; }