* 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>
235 lines
7.9 KiB
TypeScript
235 lines
7.9 KiB
TypeScript
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
|
|
import type { WebSourceMapPort } from "../../application/WebSourceLocationPorts.js";
|
|
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisCapabilityUnavailableError,
|
|
AnalysisInputError,
|
|
AnalysisOutputError,
|
|
} from "../../domain/analysisErrorCore.js";
|
|
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
|
|
import { err, ok, type Result } from "../../domain/result.js";
|
|
import { javaScriptRuntime } from "../../domain/runtimeVersion.js";
|
|
import { noRuntimeOverrideSentence } from "../../domain/browserDeadline.js";
|
|
import {
|
|
DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS,
|
|
WEB_SOURCE_MAP_LIMITS,
|
|
type WebSourceMapReport,
|
|
} from "../../domain/webSourceLocation.js";
|
|
import {
|
|
sourceMapCodecInputSchema,
|
|
sourceMapCodecReplySchema,
|
|
} from "./SourceMapCodecProtocol.js";
|
|
import {
|
|
SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES,
|
|
SourceMapCleanupFailure,
|
|
runSourceMapCommand,
|
|
type SourceMapCleanupOwner,
|
|
type SourceMapDecoderDependencies,
|
|
} from "./SourceMapCommand.js";
|
|
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
|
|
import { jsonParts } from "../../domain/jsonSerialization.js";
|
|
const OPERATION = "trace_web_source_location";
|
|
|
|
/** Integrate the pinned upstream codec through an independently bounded owned process. */
|
|
export class SourceMapDecoder implements WebSourceMapPort {
|
|
#tail: Promise<void> = Promise.resolve();
|
|
#closing = false;
|
|
#closePromise: Promise<void> | undefined;
|
|
#pendingCleanup: SourceMapCleanupOwner | undefined;
|
|
|
|
constructor(readonly dependencies: SourceMapDecoderDependencies = {}) {}
|
|
/** Return complete point evidence after process and private-root cleanup. */
|
|
trace(
|
|
input: Parameters<WebSourceMapPort["trace"]>[0],
|
|
options?: ExecutionOptions,
|
|
): Promise<Result<WebSourceMapReport, AnalysisError>> {
|
|
if (this.#closing)
|
|
return Promise.resolve(
|
|
err(
|
|
new ProviderCleanupError(
|
|
"source-map-decoder",
|
|
[],
|
|
{ reason: "Provider is closing" },
|
|
{ operation: OPERATION },
|
|
),
|
|
),
|
|
);
|
|
const operation = this.#tail.then(() =>
|
|
this.#closing
|
|
? err(
|
|
new ProviderCleanupError(
|
|
"source-map-decoder",
|
|
[],
|
|
{ reason: "Provider is closing" },
|
|
{ operation: OPERATION },
|
|
),
|
|
)
|
|
: this.#trace(input, options),
|
|
);
|
|
this.#tail = operation.then(
|
|
() => undefined,
|
|
() => undefined,
|
|
);
|
|
return operation;
|
|
}
|
|
|
|
async #trace(
|
|
input: Parameters<WebSourceMapPort["trace"]>[0],
|
|
options?: ExecutionOptions,
|
|
): Promise<Result<WebSourceMapReport, AnalysisError>> {
|
|
const cleanupFailure = await this.#retryCleanup();
|
|
if (cleanupFailure !== undefined) return err(cleanupFailure);
|
|
if (options?.signal?.aborted)
|
|
return err(
|
|
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
|
|
);
|
|
const request = sourceMapCodecInputSchema.safeParse({
|
|
text: input.text,
|
|
url: input.url,
|
|
position: input.position,
|
|
});
|
|
if (!request.success)
|
|
return err(new AnalysisInputError(OPERATION, { cause: request.error }));
|
|
const capacityFailure = () =>
|
|
new ArtifactOperationError(
|
|
OPERATION,
|
|
"limit",
|
|
undefined,
|
|
`${input.path}: Source-map input exceeds the ${String(WEB_SOURCE_MAP_LIMITS.mapBytes)}-byte map or ${String(WEB_SOURCE_MAP_LIMITS.outputBytes)}-byte encoded request budget. ${noRuntimeOverrideSentence("Those budgets stay fixed.", "decode_timeout_ms")}`,
|
|
);
|
|
if (Buffer.byteLength(input.text) > WEB_SOURCE_MAP_LIMITS.mapBytes)
|
|
return err(capacityFailure());
|
|
const encoded: string[] = [];
|
|
let encodedBytes = 0;
|
|
for (const part of jsonParts(request.data)) {
|
|
const bytes = Buffer.byteLength(part);
|
|
if (bytes > WEB_SOURCE_MAP_LIMITS.outputBytes - encodedBytes)
|
|
return err(capacityFailure());
|
|
encoded.push(part);
|
|
encodedBytes += bytes;
|
|
}
|
|
if (options?.signal?.aborted)
|
|
return err(
|
|
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
|
|
);
|
|
const serialized = encoded.join("");
|
|
const response = await runSourceMapCommand(
|
|
serialized,
|
|
input.path,
|
|
options,
|
|
this.dependencies,
|
|
input.decodeTimeoutMs ?? DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS,
|
|
);
|
|
if (!response.ok) {
|
|
if (response.error instanceof SourceMapCleanupFailure)
|
|
this.#pendingCleanup = response.error.cleanupOwner;
|
|
return response;
|
|
}
|
|
let value: unknown;
|
|
try {
|
|
value = JSON.parse(response.value);
|
|
} catch (cause: unknown) {
|
|
return err(
|
|
new AnalysisOutputError(
|
|
OPERATION,
|
|
`Malformed codec JSON reply: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
),
|
|
);
|
|
}
|
|
const reply = sourceMapCodecReplySchema.safeParse(value);
|
|
if (!reply.success)
|
|
return err(
|
|
new AnalysisOutputError(
|
|
OPERATION,
|
|
`Malformed codec reply: ${reply.error.message}`,
|
|
),
|
|
);
|
|
if (options?.signal?.aborted)
|
|
return err(
|
|
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
|
|
);
|
|
if (reply.data.state === "failure")
|
|
return codecFailure(input.path, reply.data.reason, reply.data.message);
|
|
const runtimeFailure = codecRuntimeFailure(reply.data.report.runtime);
|
|
return runtimeFailure === undefined
|
|
? ok(reply.data.report)
|
|
: err(new AnalysisOutputError(OPERATION, runtimeFailure));
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
this.#closing = true;
|
|
this.#closePromise ??= this.#tail
|
|
.then(async () => {
|
|
const failure = await this.#retryCleanup();
|
|
if (failure !== undefined) throw failure;
|
|
})
|
|
.catch((cause: unknown) => {
|
|
this.#closePromise = undefined;
|
|
throw cause;
|
|
});
|
|
return this.#closePromise;
|
|
}
|
|
|
|
async #retryCleanup(): Promise<AnalysisError | undefined> {
|
|
const pending = this.#pendingCleanup;
|
|
if (pending === undefined) return undefined;
|
|
const failure = await pending.close(null);
|
|
if (failure !== undefined) return failure;
|
|
this.#pendingCleanup = undefined;
|
|
return undefined;
|
|
}
|
|
}
|
|
/**
|
|
* The codec runs on this process's executable. Only a V8 report carries a heap
|
|
* limit; a Bun report must name the process limit its launcher enforced.
|
|
*/
|
|
const codecRuntimeFailure = (
|
|
runtime: WebSourceMapReport["runtime"],
|
|
): string | undefined => {
|
|
if (
|
|
runtime.id !==
|
|
(javaScriptRuntime(process.versions).id === "bun" ? "bun" : "node-v8")
|
|
)
|
|
return `Codec reported runtime ${runtime.id}, not the runtime that launched it.`;
|
|
if (
|
|
runtime.id === "node-v8" &&
|
|
runtime.v8_heap_limit_bytes > 256 * 1024 * 1024
|
|
)
|
|
return "Codec reported a V8 heap limit exceeding its independent process budget.";
|
|
if (
|
|
runtime.id === "bun" &&
|
|
runtime.process_memory_limit_bytes !== SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES
|
|
)
|
|
return "Codec reported a process memory limit other than the one its launcher enforced.";
|
|
return undefined;
|
|
};
|
|
const codecFailure = (
|
|
path: string,
|
|
reason: "format" | "unsupported" | "limit",
|
|
detail: string,
|
|
): Result<never, AnalysisError> =>
|
|
err(
|
|
reason === "limit"
|
|
? new ArtifactOperationError(
|
|
OPERATION,
|
|
"limit",
|
|
undefined,
|
|
`${path}: ${detail}`,
|
|
)
|
|
: reason === "unsupported"
|
|
? new AnalysisCapabilityUnavailableError(
|
|
"source-map-decoder",
|
|
OPERATION,
|
|
"unsupported_source_map_profile",
|
|
{ userMessage: `${path}: ${detail}` },
|
|
)
|
|
: new AnalysisInputError(OPERATION, undefined, [
|
|
{
|
|
path: ["source_map", "path"],
|
|
reason: "invalid_format",
|
|
message: `${path}: ${detail}`,
|
|
},
|
|
]),
|
|
);
|