refactor(engine): type capture failures

This commit is contained in:
James
2026-07-17 04:10:30 -04:00
parent c268f5ba85
commit acb3d81b99
8 changed files with 363 additions and 118 deletions
@@ -1,13 +1,18 @@
import { normalizeErrorMessage } from "../../utils/errorMessage.js";
import { CaptureFailure, classifyCaptureFailure } from "@hyperframes/engine";
export class CaptureStageError extends Error {
export class CaptureStageError extends CaptureFailure {
readonly browserConsole: string[];
readonly cause: unknown;
constructor(input: { cause: unknown; browserConsole: string[] }) {
super(normalizeErrorMessage(input.cause));
const classified = classifyCaptureFailure(input.cause);
super({
kind: classified.kind,
message: normalizeErrorMessage(input.cause),
cause: input.cause,
workerDiagnostics: classified.workerDiagnostics,
});
this.name = "CaptureStageError";
this.cause = input.cause;
this.browserConsole = input.browserConsole.slice();
if (input.cause instanceof Error && input.cause.stack) {
this.stack = input.cause.stack;
@@ -80,8 +80,8 @@ import {
resolveHeadlessShellPath,
applyConcreteGpuScreenshotClamp,
scaleProtocolTimeoutForComposition,
classifyCaptureFailure,
isMemoryExhaustionError,
isTransientBrowserError,
isDrawElementVerificationError,
getDrawElementVerificationDetails,
augmentProtocolTimeoutError,
@@ -816,12 +816,9 @@ export function resetCaptureAttemptProgress(job: { framesRendered?: number }): v
export function isRecoverableParallelCaptureError(error: unknown): boolean {
const message = normalizeErrorMessage(error);
return (
message.includes("[Parallel] Capture failed") &&
/Runtime\.callFunctionOn timed out|HeadlessExperimental\.beginFrame timed out|Waiting failed|timeout exceeded|timed out|Navigation timeout|Protocol error|Target closed/i.test(
message,
)
);
if (!message.includes("[Parallel] Capture failed")) return false;
const kind = classifyCaptureFailure(error).kind;
return kind === "transient_browser" || kind === "protocol_timeout";
}
/**
@@ -995,11 +992,12 @@ export async function executeDiskCaptureWithAdaptiveRetry(options: {
missingRanges = remaining;
attempt++;
} catch (error) {
const failure = classifyCaptureFailure(error, { signal: options.abortSignal });
// A cancelled render tears the browser down, which surfaces as a
// transient-looking `Target closed`. Rethrow immediately so cancellation
// never burns a retry (or logs a misleading transient-failure warning) —
// the caller's abort handling owns cancellation.
if (options.abortSignal?.aborted) {
if (failure.kind === "cancelled") {
throw error;
}
const remaining = findMissingFrameRanges(
@@ -1028,7 +1026,7 @@ export async function executeDiskCaptureWithAdaptiveRetry(options: {
// retry for the session-init phase they share.
if (
options.allowRetry &&
isTransientBrowserError(error) &&
failure.kind === "transient_browser" &&
transientRetriesUsed < MAX_TRANSIENT_CAPTURE_RETRIES
) {
transientRetriesUsed++;