mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 01:45:32 +00:00
* fix(browser): name web-bundle budgets and recover web deadlines Name the existing heap, per-script, and total web-bundle phases, and reject a per-script deadline above the total. Promote capture, CDP command, source-map decode, and source-map fetch deadlines at today's defaults. Name DevTools timeoutMs, and state that discovery, cleanup grace, byte budgets, and Electron hook retention have no runtime override. Refs #5910 Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(browser): apply selected web deadlines to the owning phase Keep HTTP and browser-socket discovery on the fixed discovery deadline so a short command_timeout_ms starts at the handshake. Apply scenario command_timeout_ms to launch startup and the primary connect session, and keep service-worker restoration on the fixed 5000 ms grace. Omit command_timeout_ms from recovery text when the operation does not accept it. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(browser): retain scenario commands that finish after their deadline A timed-out scenario command stays owned so cleanup can close a late browser or context and restore a mutation that lands after the deadline. Child storage replies use that deadline, scenario recovery names command_timeout_ms in the capture JSON, and the session test connection implements beginCleanupGrace. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(browser): keep child command timeouts as the public failure A nested command such as Page.enable that is delivered but never answered was stored as protocol_error, hiding the command name, selected deadline, and recovery. Store the typed timeout and assert that public error. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(browser): keep runtime cleanup on the fixed grace Switch runtime restoration to the cleanup grace before the first send so a short command_timeout_ms cannot fail an otherwise successful observation. Stop advertising command_timeout_ms on list_browser_targets, which never bounds a post-discovery command. Co-authored-by: morluto <morluto@users.noreply.github.com> * test(browser): prove a short command deadline cannot fail runtime cleanup Drive restoration through the runtime fixture. A cleanup command that outlives command_timeout_ms still succeeds, and a stalled restoration names the fixed 5000 ms grace. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(browser): name the effective source-map decode deadline codecBudgetLimitation reported a fixed 20-second deadline after a caller selected decode_timeout_ms. Result and Evidence limitations now name that effective millisecond deadline. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(browser): separate handshake and target-attach deadlines A browser-level Target.attachToTarget no longer shares the WebSocket handshake budget. Blocked service-worker attachment keeps discovery on its fixed deadline and applies command_timeout_ms to the later handshake and attach. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(browser): name the web-bundle resource each runtime exhausts Bun ignores the V8 old-space flag. A Windows Job reports the process commit limit, and other Bun hosts no longer suggest raising max_heap_mb. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(browser): cite the browser-socket discovery message budget WebSocket discovery enforces the 64 MiB protocol budget. Timeout recovery no longer advertises the 8 MiB HTTP document budget. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(browser): bound the blocked service-worker check before discovery The first registration check has its own fixed 10s deadline. Discovery, handshake, and Target.attachToTarget stay on their separate deadlines. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(browser): name the operand that stopped web-bundle admission Host heap pressure no longer claims the fixed result budget is exhausted. A Windows Bun allocation before the commit limit still names that limit. Co-authored-by: morluto <morluto@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
182 lines
6.1 KiB
TypeScript
182 lines
6.1 KiB
TypeScript
import type { ChildProcess } from "node:child_process";
|
|
import { readFile } from "node:fs/promises";
|
|
|
|
/** Terminal states that can prevent Chrome from publishing a CDP port. */
|
|
export type BrowserStartupFailure =
|
|
| "spawn-error"
|
|
| "exited"
|
|
| "signalled"
|
|
| "timeout";
|
|
|
|
/** Safe diagnostics for a browser process that did not publish a CDP port. */
|
|
export class BrowserStartupError extends Error {
|
|
readonly _tag = "BrowserStartupError" as const;
|
|
readonly failure: BrowserStartupFailure;
|
|
readonly executable: string;
|
|
readonly elapsedMs: number;
|
|
readonly startupTimeoutMs: number;
|
|
readonly exitCode: number | null;
|
|
readonly signalCode: NodeJS.Signals | null;
|
|
readonly stderr: string;
|
|
|
|
constructor(
|
|
diagnostics: {
|
|
readonly failure: BrowserStartupFailure;
|
|
readonly executable: string;
|
|
readonly elapsedMs: number;
|
|
readonly startupTimeoutMs: number;
|
|
readonly exitCode: number | null;
|
|
readonly signalCode: NodeJS.Signals | null;
|
|
readonly stderr: string;
|
|
},
|
|
options?: ErrorOptions,
|
|
) {
|
|
super(browserStartupMessage(diagnostics.failure, diagnostics), options);
|
|
this.failure = diagnostics.failure;
|
|
this.executable = diagnostics.executable;
|
|
this.elapsedMs = diagnostics.elapsedMs;
|
|
this.startupTimeoutMs = diagnostics.startupTimeoutMs;
|
|
this.exitCode = diagnostics.exitCode;
|
|
this.signalCode = diagnostics.signalCode;
|
|
this.stderr = diagnostics.stderr;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Single owner of the wait for Chrome to publish its DevToolsActivePort.
|
|
*
|
|
* Callers that need a different budget pass `timeoutMs`; nobody else should
|
|
* restate this number, because a per-caller literal silently changes how long
|
|
* a cold Chrome launch is tolerated before the failure names its ceiling.
|
|
*/
|
|
export const BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS = 60_000;
|
|
|
|
/** Owned child process and startup policy for Chrome CDP discovery. */
|
|
export interface BrowserStartupOptions {
|
|
readonly child: ChildProcess;
|
|
readonly executable: string;
|
|
readonly activePortPath: string;
|
|
readonly stderr: () => string;
|
|
readonly timeoutMs?: number;
|
|
readonly pollIntervalMs?: number;
|
|
}
|
|
|
|
/** Wait for Chrome's valid DevToolsActivePort or a terminal process outcome. */
|
|
export const waitForBrowserDevtoolsPort = async (
|
|
options: BrowserStartupOptions,
|
|
): Promise<number> => {
|
|
const timeoutMs = options.timeoutMs ?? BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS;
|
|
const pollIntervalMs = options.pollIntervalMs ?? 25;
|
|
const startedAt = performance.now();
|
|
const terminal = childTermination(options, startedAt, timeoutMs);
|
|
const deadline = startedAt + timeoutMs;
|
|
|
|
while (true) {
|
|
const terminalState = currentTerminalState(options.child);
|
|
if (terminalState !== null)
|
|
throw startupError(options, startedAt, timeoutMs, terminalState);
|
|
|
|
const port = await readDevtoolsPort(options.activePortPath);
|
|
if (port !== null) return port;
|
|
|
|
const remainingMs = deadline - performance.now();
|
|
if (remainingMs <= 0)
|
|
throw startupError(options, startedAt, timeoutMs, { failure: "timeout" });
|
|
|
|
const outcome = await Promise.race([
|
|
terminal,
|
|
delay(Math.min(pollIntervalMs, remainingMs)).then(() => null),
|
|
]);
|
|
if (outcome !== null) throw outcome;
|
|
}
|
|
};
|
|
|
|
type TerminalState =
|
|
| { readonly failure: "spawn-error"; readonly cause: unknown }
|
|
| { readonly failure: "exited" }
|
|
| { readonly failure: "signalled" }
|
|
| { readonly failure: "timeout" };
|
|
|
|
const childTermination = (
|
|
options: BrowserStartupOptions,
|
|
startedAt: number,
|
|
startupTimeoutMs: number,
|
|
): Promise<BrowserStartupError> =>
|
|
new Promise((resolve) => {
|
|
options.child.once("error", (cause: unknown) => {
|
|
resolve(
|
|
startupError(options, startedAt, startupTimeoutMs, {
|
|
failure: "spawn-error",
|
|
cause,
|
|
}),
|
|
);
|
|
});
|
|
options.child.once("exit", () => {
|
|
resolve(
|
|
startupError(
|
|
options,
|
|
startedAt,
|
|
startupTimeoutMs,
|
|
options.child.signalCode === null
|
|
? { failure: "exited" }
|
|
: { failure: "signalled" },
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
const currentTerminalState = (child: ChildProcess): TerminalState | null => {
|
|
if (child.signalCode !== null) return { failure: "signalled" };
|
|
if (child.exitCode !== null) return { failure: "exited" };
|
|
return null;
|
|
};
|
|
|
|
const startupError = (
|
|
options: BrowserStartupOptions,
|
|
startedAt: number,
|
|
startupTimeoutMs: number,
|
|
state: TerminalState,
|
|
): BrowserStartupError =>
|
|
new BrowserStartupError(
|
|
{
|
|
failure: state.failure,
|
|
executable: options.executable,
|
|
elapsedMs: Math.max(0, Math.round(performance.now() - startedAt)),
|
|
startupTimeoutMs,
|
|
exitCode: options.child.exitCode,
|
|
signalCode: options.child.signalCode,
|
|
stderr: options.stderr(),
|
|
},
|
|
state.failure === "spawn-error" ? { cause: state.cause } : undefined,
|
|
);
|
|
|
|
const readDevtoolsPort = async (path: string): Promise<number | null> => {
|
|
try {
|
|
const firstLine = (await readFile(path, "utf8")).trim().split("\n")[0];
|
|
const port = Number(firstLine);
|
|
return Number.isSafeInteger(port) && port > 0 && port <= 65_535
|
|
? port
|
|
: null;
|
|
} catch (cause: unknown) {
|
|
// Missing or unreadable port files mean the port is unknown.
|
|
void cause;
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const delay = (milliseconds: number): Promise<void> =>
|
|
new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
|
|
const browserStartupMessage = (
|
|
failure: BrowserStartupFailure,
|
|
diagnostics: {
|
|
readonly executable: string;
|
|
readonly elapsedMs: number;
|
|
readonly startupTimeoutMs: number;
|
|
readonly exitCode: number | null;
|
|
readonly signalCode: NodeJS.Signals | null;
|
|
readonly stderr: string;
|
|
},
|
|
): string =>
|
|
`Chrome CDP startup ${failure}; executable=${diagnostics.executable}; elapsed_ms=${String(diagnostics.elapsedMs)}; timeoutMs=${String(diagnostics.startupTimeoutMs)} (DevToolsActivePort wait; omitted means ${String(BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS)} ms); exit_code=${diagnostics.exitCode === null ? "null" : String(diagnostics.exitCode)}; signal=${diagnostics.signalCode ?? "null"}; stderr=${diagnostics.stderr.length === 0 ? "<empty>" : diagnostics.stderr}`;
|