/** * Run-scoped page-listener isolation. * * The tab worker keeps one long-lived puppeteer `Page` per tab and executes many * `run` cells against it. Listeners registered by a run cell used to stay * attached forever, so a second cell registering `page.on('dialog', d => * d.accept())` produced two accepts for the same dialog. The second * `Dialog.accept()` throws `Cannot accept dialog which is already handled!`; * because that happens inside puppeteer's event dispatch it escapes as a * process-level `uncaughtException`/`unhandledRejection`, which DSH treats as * fatal and exits the host (desktop crash). * * This module installs a per-run shim over the five EventEmitter registration * methods that run cells may use. Registration wraps the listener so that a * synchronous throw or a rejected promise is reported instead of escaping, and * tracks the wrapper so `dispose()` can remove exactly the listeners that the * run added. Listeners installed before the run (the tool's own dialog handler) * are never touched. * * Implementation notes: * - No `Proxy`: puppeteer's `Page` is full of private fields (`#client` …) and a * proxy would make those accesses throw. Instead the shim temporarily owns the * instance properties and simply shadows the prototype methods. * - `once` is *not* delegated to the host's `once`: puppeteer's `once()` wraps * the handler internally and its `off()` cannot find that inner wrapper, so * run-end cleanup would leak it. The wrapper removes itself instead. * - Removal (`off`/`removeListener`) is shimmed too, so a run cell that removes * its own listener removes the wrapper rather than silently failing. */ export type RunListenerEvent = string | symbol; export type RunListenerErrorReporter = (event: RunListenerEvent, error: unknown) => void; export interface RunListenerIsolation { /** Registration/removal methods actually shadowed on the host. */ readonly methods: readonly string[]; /** Number of run-registered listeners that are still attached. */ readonly pending: number; /** Remove every run-registered listener and restore the host's own methods. */ dispose(): void; } /** * Wrap every listener a run cell registers on `host` (a puppeteer page) so that * it can be removed at run end and so that listener errors are reported instead * of escaping as process-level failures. * * @param host - the page (or any EventEmitter-like object) to shim. * @param report - called with every suppressed listener error; must not throw. */ export declare function installRunListenerIsolation(host: object, report: RunListenerErrorReporter): RunListenerIsolation; export interface DialogLike { handled?: boolean; accept(text?: string): Promise; dismiss(): Promise; } export type DialogPolicy = "accept" | "dismiss"; export type DialogSettlement = { status: "handled"; } | { status: "skipped"; } | { status: "failed"; error: unknown; }; /** Puppeteer marks a dialog `handled` as soon as accept()/dismiss() is called. */ export declare function isDialogAlreadyHandled(dialog: unknown): boolean; /** * Accept/dismiss a dialog exactly once, never throwing. * * If another listener (a stale run cell) already handled it, skip instead of * triggering puppeteer's `Cannot accept dialog which is already handled!` * assertion. Any accept/dismiss failure is returned as data, because the caller * runs inside puppeteer's event dispatch where a throw kills the host process. */ export declare function settleDialog(dialog: DialogLike, policy: DialogPolicy): Promise;