export type OutputHandler = (payload: T) => void | Promise; export interface OutputHost extends HTMLElement { __wrnexusOutputHandlers?: Map>; } export function registerOutputHandler( host: OutputHost, name: string, handler: OutputHandler, ): () => void { const registry = (host.__wrnexusOutputHandlers ??= new Map()); const handlers = registry.get(name) ?? new Set(); handlers.add(handler as OutputHandler); registry.set(name, handlers); return () => { handlers.delete(handler as OutputHandler); if (!handlers.size) registry.delete(name); }; } /** * Surface a throwing output handler. * * Callers of `invokeOutput` do not await it — an output call is fire-and-forget * from the component's point of view. A handler that throws therefore becomes an * unhandled rejection that never reaches the console, and the symptom is a * control that silently does nothing. Reporting here turns that into a named * diagnostic instead of a dead button. */ function reportOutputError(host: OutputHost, name: string, error: unknown): void { const code = "WRN-DEV-OUTPUT-HANDLER-ERROR"; const message = `The handler bound to output '${name}' threw. The control will appear to do nothing.`; console.error(`[${code}] ${message}`, error); try { window.dispatchEvent( new CustomEvent("wrnexus:diagnostic", { detail: { code, message, hydrationId: host.getAttribute ? host.getAttribute("data-wrn-hydration") : null, detail: { output: name, error: String(error) }, }, }), ); } catch { // CustomEvent can be unavailable in minimal DOM test environments. } } export async function invokeOutput(host: OutputHost, name: string, payload?: T): Promise { const handlers = host.__wrnexusOutputHandlers?.get(name); if (handlers?.size) { for (const handler of handlers) { try { await handler(payload); } catch (error) { reportOutputError(host, name, error); } } return; } // Compatibility path for legacy listeners outside a hydrated WRN parent. host.dispatchEvent(new CustomEvent(name, { detail: payload })); host.dispatchEvent(new CustomEvent(`wrnexus:${name}`, { detail: payload })); } export function createOutputProxy void>>( host: OutputHost, ): T { return new Proxy({} as T, { get: (_target, property) => (payload?: unknown) => invokeOutput(host, String(property), payload), }); }