fix(plugins): correct JSDoc for callHandlerWithTimeout timeout behavior

This commit is contained in:
Jerry-Xin 2026-03-17 13:37:15 +08:00
parent f7681e6fe9
commit 20615bbb13

View File

@ -5,6 +5,7 @@
* error handling, priority ordering, and async support.
*/
import { withTimeout } from "../node-host/with-timeout.js";
import { concatOptionalTextSegments } from "../shared/text/join-segments.js";
import type { PluginRegistry } from "./registry.js";
import type {
@ -108,10 +109,15 @@ export type HookRunnerLogger = {
error: (message: string) => void;
};
/** Default timeout for async plugin hook handlers (ms). */
const DEFAULT_HOOK_TIMEOUT_MS = 10_000;
export type HookRunnerOptions = {
logger?: HookRunnerLogger;
/** If true, errors in hooks will be caught and logged instead of thrown */
catchErrors?: boolean;
/** Per-handler timeout for async hooks (ms). Defaults to 10 000. Set 0 to disable. */
hookTimeoutMs?: number;
};
export type PluginTargetedInboundClaimOutcome =
@ -159,6 +165,27 @@ function getHooksForNameAndPlugin<K extends PluginHookName>(
export function createHookRunner(registry: PluginRegistry, options: HookRunnerOptions = {}) {
const logger = options.logger;
const catchErrors = options.catchErrors ?? true;
const hookTimeoutMs =
typeof options.hookTimeoutMs === "number" && options.hookTimeoutMs > 0
? options.hookTimeoutMs
: options.hookTimeoutMs === 0
? undefined // explicitly disabled
: DEFAULT_HOOK_TIMEOUT_MS;
/**
* Execute a single async handler with the configured timeout.
* Throws on timeout; callers catch via handleHookError.
*/
async function callHandlerWithTimeout<T>(
fn: () => Promise<T>,
hookName: PluginHookName,
pluginId: string,
): Promise<T> {
if (!hookTimeoutMs) {
return fn();
}
return withTimeout(() => fn(), hookTimeoutMs, `${hookName} handler from ${pluginId}`);
}
const mergeBeforeModelResolve = (
acc: PluginHookBeforeModelResolveResult | undefined,
@ -253,7 +280,11 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
const promises = hooks.map(async (hook) => {
try {
await (hook.handler as (event: unknown, ctx: unknown) => Promise<void>)(event, ctx);
await callHandlerWithTimeout(
() => (hook.handler as (event: unknown, ctx: unknown) => Promise<void>)(event, ctx),
hookName,
hook.pluginId,
);
} catch (err) {
handleHookError({ hookName, pluginId: hook.pluginId, error: err });
}
@ -283,9 +314,11 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
for (const hook of hooks) {
try {
const handlerResult = await (
hook.handler as (event: unknown, ctx: unknown) => Promise<TResult>
)(event, ctx);
const handlerResult = await callHandlerWithTimeout(
() => (hook.handler as (event: unknown, ctx: unknown) => Promise<TResult>)(event, ctx),
hookName,
hook.pluginId,
);
if (handlerResult !== undefined && handlerResult !== null) {
if (mergeResults && result !== undefined) {
@ -317,7 +350,23 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
logger?.debug?.(`[hooks] running ${hookName} (${hooks.length} handlers, first-claim wins)`);
return await runClaimingHooksList(hooks, hookName, event, ctx);
for (const hook of hooks) {
try {
const handlerResult = await callHandlerWithTimeout(
() =>
(hook.handler as (event: unknown, ctx: unknown) => Promise<TResult | void>)(event, ctx),
hookName,
hook.pluginId,
);
if (handlerResult?.handled) {
return handlerResult;
}
} catch (err) {
handleHookError({ hookName, pluginId: hook.pluginId, error: err });
}
}
return undefined;
}
async function runClaimingHookForPlugin<
@ -338,23 +387,14 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
`[hooks] running ${hookName} for ${pluginId} (${hooks.length} handlers, targeted)`,
);
return await runClaimingHooksList(hooks, hookName, event, ctx);
}
async function runClaimingHooksList<
K extends PluginHookName,
TResult extends { handled: boolean },
>(
hooks: Array<PluginHookRegistration<K> & { pluginId: string }>,
hookName: K,
event: Parameters<NonNullable<PluginHookRegistration<K>["handler"]>>[0],
ctx: Parameters<NonNullable<PluginHookRegistration<K>["handler"]>>[1],
): Promise<TResult | undefined> {
for (const hook of hooks) {
try {
const handlerResult = await (
hook.handler as (event: unknown, ctx: unknown) => Promise<TResult | void>
)(event, ctx);
const handlerResult = await callHandlerWithTimeout(
() =>
(hook.handler as (event: unknown, ctx: unknown) => Promise<TResult | void>)(event, ctx),
hookName,
hook.pluginId,
);
if (handlerResult?.handled) {
return handlerResult;
}
@ -400,9 +440,12 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
let firstError: string | null = null;
for (const hook of hooks) {
try {
const handlerResult = await (
hook.handler as (event: unknown, ctx: unknown) => Promise<TResult | void>
)(event, ctx);
const handlerResult = await callHandlerWithTimeout(
() =>
(hook.handler as (event: unknown, ctx: unknown) => Promise<TResult | void>)(event, ctx),
hookName,
hook.pluginId,
);
if (handlerResult?.handled) {
return { status: "handled", result: handlerResult };
}