import { Runtime } from "#Source/environment/base.index.ts" import { normalizeNodejsExceptionRecord } from "./normalize.ts" import type { ExceptionListenerCleanup, NodejsExceptionRecord, ObserveExceptionsOptions, } from "./types.ts" /** * @description 表示 Nodejs 侧异常监听回调。 */ export type NodejsExceptionListener = (record: NodejsExceptionRecord) => void /** * @description 监听 Node.js 的 `uncaughtExceptionMonitor` 事件。 */ export const onNodejsUncaughtExceptionMonitor = ( listener: NodejsExceptionListener, options?: ObserveExceptionsOptions | undefined, ): ExceptionListenerCleanup => { return Runtime.useNodejs( (context) => { const currentProcess = context.globalThis.process const internalListener = (exception: unknown, origin: unknown): void => { listener( normalizeNodejsExceptionRecord({ source: "nodejs.uncaught-exception-monitor", exception, timestamp: options?.captureTimestamp?.() ?? Date.now(), originalEvent: { exception, origin }, origin: typeof origin === "string" ? origin : undefined, }), ) } currentProcess.on("uncaughtExceptionMonitor", internalListener) return () => { if (typeof currentProcess.off === "function") { currentProcess.off("uncaughtExceptionMonitor", internalListener) return } currentProcess.removeListener?.("uncaughtExceptionMonitor", internalListener) } }, () => { return (): void => { return undefined } }, ) } /** * @description 监听 Node.js 的 `uncaughtException` 事件。 */ export const onNodejsUncaughtException = ( listener: NodejsExceptionListener, options?: ObserveExceptionsOptions | undefined, ): ExceptionListenerCleanup => { return Runtime.useNodejs( (context) => { const currentProcess = context.globalThis.process const internalListener = (exception: unknown, origin: unknown): void => { listener( normalizeNodejsExceptionRecord({ source: "nodejs.uncaught-exception", exception, timestamp: options?.captureTimestamp?.() ?? Date.now(), originalEvent: { exception, origin }, origin: typeof origin === "string" ? origin : undefined, }), ) } currentProcess.on("uncaughtException", internalListener) return () => { if (typeof currentProcess.off === "function") { currentProcess.off("uncaughtException", internalListener) return } currentProcess.removeListener?.("uncaughtException", internalListener) } }, () => { return (): void => { return undefined } }, ) } /** * @description 监听 Node.js 的 `unhandledRejection` 事件。 */ export const onNodejsUnhandledRejection = ( listener: NodejsExceptionListener, options?: ObserveExceptionsOptions | undefined, ): ExceptionListenerCleanup => { return Runtime.useNodejs( (context) => { const currentProcess = context.globalThis.process const internalListener = (reason: unknown, promise: unknown): void => { listener( normalizeNodejsExceptionRecord({ source: "nodejs.unhandled-rejection", exception: reason, timestamp: options?.captureTimestamp?.() ?? Date.now(), originalEvent: { reason, promise }, promise: promise instanceof Promise ? promise : undefined, }), ) } currentProcess.on("unhandledRejection", internalListener) return () => { if (typeof currentProcess.off === "function") { currentProcess.off("unhandledRejection", internalListener) return } currentProcess.removeListener?.("unhandledRejection", internalListener) } }, () => { return (): void => { return undefined } }, ) } /** * @description 表示批量监听 Nodejs 异常时可选的配置。 * * 未显式传入时,三个 Node.js 入口默认都会启用;仅当对应选项为 `false` 时才跳过该入口。 */ export interface ObserveNodejsExceptionsOptions extends ObserveExceptionsOptions { includeUncaughtExceptionMonitor?: boolean | undefined includeUncaughtException?: boolean | undefined includeUnhandledRejection?: boolean | undefined } /** * @description 批量监听 Nodejs 侧全局异常入口。 * * 默认会同时监听 `uncaughtExceptionMonitor`、`uncaughtException` 和 `unhandledRejection`。 * 如需关闭某个入口,请将对应的 `include*` 选项显式设为 `false`。 */ export const observeNodejsExceptions = ( listener: NodejsExceptionListener, options?: ObserveNodejsExceptionsOptions | undefined, ): ExceptionListenerCleanup => { const { includeUncaughtExceptionMonitor, includeUncaughtException, includeUnhandledRejection } = options ?? {} const cleanups: ExceptionListenerCleanup[] = [] if (includeUncaughtExceptionMonitor !== false) { cleanups.push(onNodejsUncaughtExceptionMonitor(listener, options)) } if (includeUncaughtException !== false) { cleanups.push(onNodejsUncaughtException(listener, options)) } if (includeUnhandledRejection !== false) { cleanups.push(onNodejsUnhandledRejection(listener, options)) } return () => { for (const cleanup of cleanups) { cleanup() } } }