import { GetCommunication, IMessageEvent } from '@nitrots/nitro-renderer'; export interface AwaitMessageEventInit { timeoutMs?: number; signal?: AbortSignal; accept?: (event: T) => boolean; /** * Synchronous mapper that runs INSIDE the subscribe callback, while * the parser is still valid. Whatever it returns is what the Promise * resolves to. **MUST** be used for any read of `event.getParser()` — * the renderer recycles parser instances (the `_parser` field is * nulled / repopulated for the next packet) so reading the parser * AFTER the await microtask gives back null fields. Snapshot the * data here, return a plain object/value, then your async code is * safe. */ select?: (event: T) => R; } const DEFAULT_TIMEOUT_MS = 15_000; /** * One-shot Promise adapter over the renderer's CommunicationManager.subscribeMessage. * Resolves on the first matching event, rejects on timeout / abort / connection error. * Used by request-response patterns (e.g. housekeeping lookups) that need a Promise * facade over the underlying packet stream. * * **Read the parser inside `select`, not after the await.** See the * AwaitMessageEventInit.select javadoc — the renderer recycles parsers, * so post-await reads come back null. */ export const awaitMessageEvent = (eventCtor: new (callback: (event: T) => void) => T, init: AwaitMessageEventInit = {}): Promise => { const { timeoutMs = DEFAULT_TIMEOUT_MS, signal, accept, select } = init; return new Promise((resolve, reject) => { if(signal?.aborted) { reject(new DOMException('aborted', 'AbortError')); return; } const communication = GetCommunication(); if(!communication || !communication.connection) { reject(new Error('no_connection')); return; } let settled = false; let unsubscribe: (() => void) | null = null; let timer: ReturnType | null = null; let onAbort: (() => void) | null = null; const cleanup = () => { settled = true; if(unsubscribe) unsubscribe(); unsubscribe = null; if(timer) clearTimeout(timer); timer = null; if(onAbort && signal) signal.removeEventListener('abort', onAbort); onAbort = null; }; unsubscribe = communication.subscribeMessage(eventCtor, event => { if(settled) return; if(accept && !accept(event)) return; // Snapshot the data synchronously: post-await reads of the // event's parser come back null because the renderer recycles // parser instances between packets. If no select supplied, // resolve with the raw event for backwards-compat callers // that don't touch the parser. let snapshot: R; try { snapshot = select ? select(event) : (event as unknown as R); } catch(err) { cleanup(); reject(err instanceof Error ? err : new Error(String(err))); return; } cleanup(); resolve(snapshot); }); timer = setTimeout(() => { if(settled) return; cleanup(); reject(new Error('timeout')); }, timeoutMs); if(signal) { onAbort = () => { if(settled) return; cleanup(); reject(new DOMException('aborted', 'AbortError')); }; signal.addEventListener('abort', onAbort, { once: true }); } }); };