refactor(browser): build browser cancellations with a required reason (#10581)

* refactor(errors): require a reason for every analysis cancellation

Add cancelledBy, the one constructor for AnalysisCancelledError. Its
reason argument is required: an aborted signal, an earlier cancellation,
a reason carried from an inner failure, or an OwnerCancellation when REA
closed the work's owner itself.

Closed adb, apktool, firmware, sqlite, gdb, web-bundle analyzer and
binary-session record owners now report OwnerCancellation.ownerClosed
instead of an absent reason. Firmware's predecessor wait returns its
cancellation as a Result, removing an unreachable reason-less fallback.

An architecture guard rejects new direct constructions, with a counted
per-file baseline that can only shrink.

* refactor(errors): share one aborted-signal predicate with cancelledBy

* refactor(process): return a departing caller's reason from a shared startup wait

* refactor(browser): build browser cancellations with a required reason

Migrate every AnalysisCancelledError construction under src/browser,
src/javascript, src/electron, src/inspector and src/captures to
cancelledBy and lower the guard baseline from 671 to 572. Service-worker
setup aborts with OwnerCancellation.ownerClosed on close.

* refactor(providers): build decompiler-provider cancellations with a required reason (#10587)

Migrate every AnalysisCancelledError construction under src/ghidra,
src/ida, src/cutter, src/jeb and src/kuna to cancelledBy and lower the
guard baseline from 671 to 572. Closed Kuna target commands report
OwnerCancellation.ownerClosed instead of an absent reason, and Kuna and
JEB close abort their lifetimes with that typed reason.

* fix(errors): read the reason of any signal-shaped cancellation source

* test(ci): cover the counted-baseline comparison behind the architecture guard

* refactor(electron): drop an unused error import from the fuse failure mapper
This commit is contained in:
morluto authored and GitHub committed 2026-11-15 17:46:20 +08:00
1 parent a761efda70
commit b3cdbb92ee
41 files changed
+298 -425

No files matched your search

@@ -79,47 +79,7 @@
"src/application/releaseExposure/ReleaseExposureService.ts": 3,
"src/application/reverse/ReverseEngineeringService.ts": 12,
"src/application/wasm/WasmInterfaceComparisonService.ts": 2,
"src/browser/CdpBrowserProvider.ts": 4,
"src/browser/CdpCaptureValues.ts": 2,
"src/browser/CdpConnection.ts": 4,
"src/browser/CdpElectronInspection.ts": 1,
"src/browser/CdpElectronProvider.ts": 3,
"src/browser/CdpElectronScripts.ts": 1,
"src/browser/CdpElectronWorkers.ts": 1,
"src/browser/CdpEndpoint.ts": 4,
"src/browser/CdpObservationSession.ts": 1,
"src/browser/CdpScriptSourceCapture.ts": 1,
"src/browser/CdpTargetSessionOwner.ts": 4,
"src/browser/ElectronFileScope.ts": 2,
"src/browser/IsolatedWebBundleAnalyzer.ts": 1,
"src/browser/PlaywrightBrowserScenarioProvider.ts": 2,
"src/browser/PlaywrightElectronActiveActions.ts": 1,
"src/browser/PlaywrightElectronActiveProvider.ts": 1,
"src/browser/PlaywrightExecutionBoundary.ts": 2,
"src/browser/WebBundleAnalysisWorker.ts": 8,
"src/browser/WebSourceMapFetcher.ts": 9,
"src/browser/assets/PublishWebScripts.ts": 3,
"src/browser/assets/WebArtifactReads.ts": 2,
"src/browser/execution/CdpExecutionCapture.ts": 1,
"src/browser/execution/CdpExecutionWindow.ts": 1,
"src/browser/execution/CdpWebRuntimeProvider.ts": 2,
"src/browser/history/CaptureFailures.ts": 3,
"src/browser/history/HistoricalCaptureDecoder.ts": 1,
"src/browser/history/HistoricalCaptureReply.ts": 1,
"src/browser/history/playwrightTrace/PlaywrightTraceDecoder.ts": 2,
"src/browser/history/playwrightTrace/TraceFailure.ts": 1,
"src/browser/modules/NativeModuleResolver.ts": 3,
"src/captures/TsharkCaptureProvider.ts": 2,
"src/electron/ElectronFuseFailures.ts": 3,
"src/gdb/GdbSessionManager.ts": 13,
"src/inspector/JavaScriptRuntimeScope.ts": 1,
"src/inspector/V8InspectorProvider.ts": 2,
"src/javascript/analysis/JavaScriptAnalysisWorker.ts": 5,
"src/javascript/recovery/RecoveryFiles.ts": 3,
"src/javascript/recovery/WakaruCommand.ts": 2,
"src/javascript/recovery/WakaruProvider.ts": 2,
"src/javascript/sourceMaps/SourceMapCommand.ts": 4,
"src/javascript/sourceMaps/SourceMapDecoder.ts": 3,
"src/native/AppleDispatchMetadata.ts": 3,
"src/native/LldbCallTracer.ts": 2,
"src/native/NativeCallObservation.ts": 2,
+11 -12
View File
@@ -54,8 +54,9 @@ import {
} from "../domain/analysisErrorBase.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import {
AnalysisCancelledError,
AnalysisInputError,
cancelledBy,
carriedReason,
} from "../domain/analysisErrorCore.js";
import { BrowserObservationError } from "../domain/browserObservationError.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
@@ -334,12 +335,13 @@ export class CdpBrowserProvider implements BrowserObservationPort {
// Capture and analyzer work can still throw the exact caller reason.
if (options.signal?.aborted === true && cause === options.signal.reason) {
return err(
new AnalysisCancelledError("analyze_web_bundle", {
cause,
...(captured === undefined
cancelledBy(
"analyze_web_bundle",
carriedReason(cause),
captured === undefined
? {}
: { partialObservation: captured.inspection }),
}),
: { partialObservation: captured.inspection },
),
);
}
return err(providerError(cause, "analyze_web_bundle", options.signal));
@@ -615,11 +617,8 @@ const withTargetSession = async <
if (outcome.ok && options.signal?.aborted === true)
outcome = err(
partialObservation === undefined
? new AnalysisCancelledError(operation, {
cause: options.signal.reason,
})
: new AnalysisCancelledError(operation, {
cause: options.signal.reason,
? cancelledBy(operation, options.signal)
: cancelledBy(operation, options.signal, {
partialObservation: partialObservation(outcome.value),
}),
);
@@ -724,7 +723,7 @@ const providerError = (
: cause instanceof AnalysisError
? cause
: signal?.aborted === true
? new AnalysisCancelledError(operation, { cause })
? cancelledBy(operation, carriedReason(cause))
: new ProviderAdapterError(
CDP_BROWSER_PROVIDER_IDENTITY.id,
operation,
+3 -6
View File
@@ -1,5 +1,5 @@
import { MAX_NODE_TIMER_DELAY_MS } from "../domain/analysisResourceLimits.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { cancelledBy, carriedReason } from "../domain/analysisErrorCore.js";
import type { BrowserObservationOperation } from "../domain/browserObservationErrors.js";
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
@@ -82,8 +82,7 @@ export const delayWithCancellation = async (
): Promise<void> => {
let remaining = durationMs;
while (remaining > 0) {
if (signal?.aborted === true)
throw new AnalysisCancelledError(operation, { cause: signal.reason });
if (signal?.aborted === true) throw cancelledBy(operation, signal);
const delay = Math.min(remaining, MAX_NODE_TIMER_DELAY_MS);
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
@@ -93,9 +92,7 @@ export const delayWithCancellation = async (
const onAbort = (): void => {
clearTimeout(timer);
signal?.removeEventListener("abort", onAbort);
reject(
new AnalysisCancelledError(operation, { cause: signal?.reason }),
);
reject(cancelledBy(operation, carriedReason(signal?.reason)));
};
signal?.addEventListener("abort", onAbort, { once: true });
if (signal?.aborted === true) onAbort();
+5 -13
View File
@@ -2,7 +2,7 @@ import { MAX_NODE_TIMER_DELAY_MS } from "../domain/analysisResourceLimits.js";
import WebSocket, { type RawData } from "ws";
import { isUtf8 } from "node:buffer";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { cancelledBy, carriedReason } from "../domain/analysisErrorCore.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import { BrowserObservationError } from "../domain/browserObservationError.js";
import { CdpCommandRejection } from "./CdpCommandRejection.js";
@@ -144,10 +144,7 @@ class CdpTransportCore {
throw new BrowserObservationError(operation, "protocol_error");
if (this.#closed || this.socket.readyState !== WebSocket.OPEN)
throw this.transportError(this.#transportFailure, operation);
if (signal?.aborted === true)
throw new AnalysisCancelledError(operation, {
cause: signal.reason,
});
if (signal?.aborted === true) throw cancelledBy(operation, signal);
const id = this.#nextId;
this.#nextId += 1;
const message = JSON.stringify({
@@ -161,11 +158,7 @@ class CdpTransportCore {
const pending = this.#pending.get(id);
if (pending === undefined) return;
this.#complete(id, pending);
reject(
new AnalysisCancelledError(operation, {
cause: signal?.reason,
}),
);
reject(cancelledBy(operation, carriedReason(signal?.reason)));
};
const failTimeout = (): void => {
const pending = this.#pending.get(id);
@@ -501,8 +494,7 @@ export class CdpConnection {
},
onConnected?: (connection: CdpConnection) => void,
): Promise<CdpConnection> {
if (signal?.aborted === true)
throw new AnalysisCancelledError(operation, { cause: signal.reason });
if (signal?.aborted === true) throw cancelledBy(operation, signal);
const maxPayloadBytes =
limits?.maxPayloadBytes ?? WEB_RUNTIME_LIMITS.protocolBytes;
const commandTimeoutMs =
@@ -655,7 +647,7 @@ const waitForOpen = async (
const onAbort = (): void => {
cleanup();
terminateSocket(socket);
reject(new AnalysisCancelledError(operation, { cause: signal?.reason }));
reject(cancelledBy(operation, carriedReason(signal?.reason)));
};
const cleanup = (): void => {
clearTimeout(timer);
+2 -4
View File
@@ -7,7 +7,7 @@ import type {
InspectElectronPageInput,
} from "../domain/javascript/electronObservation.js";
import { BrowserObservationError } from "../domain/browserObservationError.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { cancelledBy } from "../domain/analysisErrorCore.js";
import type { CdpConnection, CdpEvent } from "./CdpConnection.js";
import type { CdpEndpointDiscovery, CdpEndpointTarget } from "./CdpEndpoint.js";
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
@@ -867,9 +867,7 @@ const selectedMainFrame = (
const throwIfElectronInspectionCancelled = (signal?: AbortSignal): void => {
if (signal?.aborted === true)
throw new AnalysisCancelledError("inspect_electron_page", {
cause: signal.reason,
});
throw cancelledBy("inspect_electron_page", signal);
};
const report = async (
+4 -7
View File
@@ -18,7 +18,7 @@ import {
} from "../domain/javascript/electronObservation.js";
import { selectedCdpCommandTimeoutMs } from "../domain/browserDeadline.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { cancelledBy } from "../domain/analysisErrorCore.js";
import { BrowserObservationError } from "../domain/browserObservationError.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { err, ok, type Result } from "../domain/result.js";
@@ -238,8 +238,7 @@ export class CdpElectronProvider implements ElectronObservationPort {
if (fileFailure !== undefined) return err(fileFailure);
return err(
options.signal?.aborted === true
? new AnalysisCancelledError("list_electron_targets", {
cause: options.signal.reason,
? cancelledBy("list_electron_targets", options.signal, {
diagnostics: {
completed_prefix: completedPrefix,
},
@@ -334,8 +333,7 @@ export class CdpElectronProvider implements ElectronObservationPort {
}
const finalCancellation =
result.ok && options.signal?.aborted === true
? new AnalysisCancelledError("inspect_electron_page", {
cause: options.signal.reason,
? cancelledBy("inspect_electron_page", options.signal, {
partialObservation: {
kind: "electron-page-observation",
inspection: result.value,
@@ -453,8 +451,7 @@ const throwIfElectronOperationCancelled = (
signal: AbortSignal | undefined,
operation: "list_electron_targets" | "inspect_electron_page",
): void => {
if (signal?.aborted === true)
throw new AnalysisCancelledError(operation, { cause: signal.reason });
if (signal?.aborted === true) throw cancelledBy(operation, signal);
};
const providerError = (
+2 -4
View File
@@ -15,7 +15,7 @@ import type {
} from "./CdpElectronScriptEvents.js";
import { authorizedElectronFile } from "./ElectronFileScope.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { cancelledBy } from "../domain/analysisErrorCore.js";
import { selectedCdpCommandTimeoutMs } from "../domain/browserDeadline.js";
import { CdpScriptSourceBudget } from "./CdpScriptSourceBudget.js";
@@ -129,7 +129,5 @@ export const captureElectronScripts = async (
const throwIfCallerCancelled = (signal: AbortSignal | undefined): void => {
if (signal?.aborted === true)
throw new AnalysisCancelledError("inspect_electron_page", {
cause: signal.reason,
});
throw cancelledBy("inspect_electron_page", signal);
};
+2 -4
View File
@@ -9,7 +9,7 @@ import type { CdpEndpointTarget } from "./CdpEndpoint.js";
import { optionalCdpCommand } from "./CdpOptionalCommand.js";
import { authorizedElectronFile } from "./ElectronFileScope.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { cancelledBy } from "../domain/analysisErrorCore.js";
import {
parseCdpWorkerTarget,
workerTargetOwnership,
@@ -92,7 +92,5 @@ export const captureElectronWorkers = async (input: {
const throwIfElectronWorkerCaptureCancelled = (signal?: AbortSignal): void => {
if (signal?.aborted === true)
throw new AnalysisCancelledError("inspect_electron_page", {
cause: signal.reason,
});
throw cancelledBy("inspect_electron_page", signal);
};
+9 -7
View File
@@ -9,7 +9,11 @@ import {
cdpDiscoveryTimeoutRecovery,
noRuntimeOverrideSentence,
} from "../domain/browserDeadline.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import {
AnalysisCancelledError,
cancelledBy,
carriedReason,
} from "../domain/analysisErrorCore.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import { BrowserObservationError } from "../domain/browserObservationError.js";
import type { BrowserObservationOperation } from "../domain/browserObservationErrors.js";
@@ -194,7 +198,7 @@ export const withCdpDiscoveryDeadline = async <T>(
} catch (cause: unknown) {
if (cause === abortReason) {
if (abortSource === "caller")
throw new AnalysisCancelledError(operation, { cause: abortReason });
throw cancelledBy(operation, carriedReason(abortReason));
if (abortSource === "deadline")
throw discoveryTimeout(operation, timeoutMs, "browser-socket");
}
@@ -465,7 +469,7 @@ export const readCdpJson = async (
): Promise<unknown> =>
await new Promise((resolve, reject) => {
if (signal?.aborted === true) {
reject(new AnalysisCancelledError(operation, { cause: signal.reason }));
reject(cancelledBy(operation, signal));
return;
}
let response: IncomingMessage | undefined;
@@ -486,8 +490,7 @@ export const readCdpJson = async (
} else resolve(value);
};
const onAbort = (): void => {
if (signal?.aborted === true)
finish(new AnalysisCancelledError(operation, { cause: signal.reason }));
if (signal?.aborted === true) finish(cancelledBy(operation, signal));
};
request_ = request(
url,
@@ -585,8 +588,7 @@ const endpointFailure = (
signal?: AbortSignal,
): AnalysisError => {
if (cause instanceof AnalysisError) return cause;
if (signal?.aborted === true)
return new AnalysisCancelledError(operation, { cause: signal.reason });
if (signal?.aborted === true) return cancelledBy(operation, signal);
return new BrowserObservationError(operation, "endpoint_unreachable", {
cause,
});
+3 -4
View File
@@ -4,7 +4,8 @@ import type {
WebObservationSession,
} from "../domain/browserSession.js";
import {
AnalysisCancelledError,
cancelledBy,
carriedReason,
OwnerCancellation,
} from "../domain/analysisErrorCore.js";
import { BrowserObservationError } from "../domain/browserObservationError.js";
@@ -779,9 +780,7 @@ const waitForWindow = async (
const onAbort = (): void =>
settle(() => {
reject(
new AnalysisCancelledError("observe_web_session", {
cause: signal?.reason,
}),
cancelledBy("observe_web_session", carriedReason(signal?.reason)),
);
});
signal?.addEventListener("abort", onAbort, { once: true });
+2 -4
View File
@@ -1,4 +1,4 @@
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { cancelledBy } from "../domain/analysisErrorCore.js";
import type { BrowserObservationOperation } from "../domain/browserObservationErrors.js";
import type { CdpConnection } from "./CdpConnection.js";
import type { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
@@ -33,9 +33,7 @@ export const capturePassiveCdpScriptSource = async (input: {
};
const throwIfCancelled = () => {
if (input.signal?.aborted === true)
throw new AnalysisCancelledError(input.operation, {
cause: input.signal.reason,
});
throw cancelledBy(input.operation, input.signal);
};
throwIfCancelled();
if (input.sourceBudget.timedOut) return omitForBudget();
+13 -13
View File
@@ -11,6 +11,8 @@ import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
cancelledBy,
carriedReason,
} from "../domain/analysisErrorCore.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { CdpConnection } from "./CdpConnection.js";
@@ -145,9 +147,7 @@ export class CdpTargetSessionOwner {
while (true) {
this.#assertOpen(operation);
if (options.signal?.aborted === true)
throw new AnalysisCancelledError(operation, {
cause: options.signal.reason,
});
throw cancelledBy(operation, options.signal);
const admitted = this.#browserSockets.get(url);
const socket = admitted ?? this.#candidateBrowserSockets.get(url);
if (socket === undefined) return undefined;
@@ -222,9 +222,10 @@ export class CdpTargetSessionOwner {
const onAbort = (): void => {
waiter.detachAbort();
if (!activePending.waiters.delete(waiter)) return;
const cancelError = new AnalysisCancelledError(operation, {
cause: options.signal?.reason,
});
const cancelError = cancelledBy(
operation,
carriedReason(options.signal?.reason),
);
if (activePending.waiters.size > 0) {
reject(cancelError);
return;
@@ -434,9 +435,7 @@ export class CdpTargetSessionOwner {
await this.retryPending();
this.#assertOpen(operation);
if (options.signal?.aborted === true)
throw new AnalysisCancelledError(operation, {
cause: options.signal.reason,
});
throw cancelledBy(operation, options.signal);
this.#admissions += 1;
let connection: CdpConnection | undefined;
try {
@@ -671,10 +670,11 @@ const rebindPendingConnectFailure = (
: rebound;
}
if (cause instanceof AnalysisCancelledError) {
const rebound = new AnalysisCancelledError(waiter.operation, {
cause: waiter.signal?.reason ?? cause.cause,
cleanup: cause.cleanup,
});
const rebound = cancelledBy(
waiter.operation,
carriedReason(waiter.signal?.reason ?? cause.cause),
{ cleanup: cause.cleanup },
);
return cause.cleanupIncomplete
? rebound.bindCleanupDependencies([cause])
: rebound;
+3 -6
View File
@@ -2,7 +2,7 @@ import type { Stats } from "node:fs";
import { open, realpath, stat } from "node:fs/promises";
import { isAbsolute, join, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { cancelledBy, carriedReason } from "../domain/analysisErrorCore.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
@@ -65,8 +65,7 @@ export class ElectronFileScope {
} catch (cause: unknown) {
if (cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined)
throw cause;
if (signal?.aborted === true)
throw new AnalysisCancelledError(operation, { cause: signal.reason });
if (signal?.aborted === true) throw cancelledBy(operation, signal);
throw cause;
}
}
@@ -123,9 +122,7 @@ export const mapElectronFileAuthorizationFailure = (
(physicalPrimary instanceof ArtifactReaderFailure &&
physicalPrimary.reason === "cancelled");
const primary = hasCancellation
? new AnalysisCancelledError(operation, {
cause: cancellationReason,
})
? cancelledBy(operation, carriedReason(cancellationReason))
: physicalPrimary instanceof AnalysisError
? physicalPrimary
: physicalPrimary === undefined
+1 -3
View File
@@ -13,7 +13,6 @@ import type { WebBundleScriptAnalysis } from "../domain/webBundleScriptAnalysis.
import { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisCapabilityUnavailableError,
AnalysisCancelledError,
AnalysisTimeoutError,
AnalysisResourceConstraintError,
AnalysisOutputError,
@@ -312,8 +311,7 @@ export class IsolatedWebBundleAnalyzer {
deadline.dispose();
}
if (caller.aborted && deadline.interruption !== "timeout") {
const cancelled = new AnalysisCancelledError("analyze_web_bundle", {
cause: caller.reason,
const cancelled = cancelledBy("analyze_web_bundle", caller, {
partialObservation: partial ?? inspection,
});
failure = hasPrimaryFailure
@@ -37,6 +37,8 @@ import { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
isAborted,
cancelledBy,
} from "../domain/analysisErrorCore.js";
import {
BrowserObservationError,
@@ -61,9 +63,6 @@ import {
const OPERATION = "capture_browser_scenario" as const;
import { PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY } from "./providerIdentities.js";
const signalIsAborted = (signal: AbortSignal | undefined): boolean =>
signal?.aborted === true;
type SnapshotKind = BrowserScenario["capture"]["after_each_step"][number];
const SNAPSHOT_KINDS = [
"screenshot",
@@ -326,9 +325,7 @@ const runScenario = async (
if (operationOutcome.ok && options.signal?.aborted)
operationOutcome = {
ok: false,
cause: new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
}),
cause: cancelledBy(OPERATION, options.signal),
};
const cleanup = browserCleanupObservation(cleanupOutcome.cause);
const partialObservation: BrowserScenarioPartialObservation = {
@@ -388,9 +385,8 @@ const runScenario = async (
}),
completeness,
});
if (signalIsAborted(options.signal))
throw new AnalysisCancelledError(OPERATION, {
cause: options.signal?.reason,
if (isAborted(options.signal))
throw cancelledBy(OPERATION, options.signal, {
partialObservation: {
kind: "browser-scenario-observation",
capture: {
@@ -9,7 +9,10 @@ import type {
} from "../artifacts/ArtifactResourceScope.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import {
AnalysisCancelledError,
cancelledBy,
} from "../domain/analysisErrorCore.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { BrowserObservationError } from "../domain/browserObservationError.js";
import { describeValidationFailure } from "../domain/validationFailure.js";
@@ -241,9 +244,7 @@ const releaseActionWindow = async (
error.bindCleanupResourceOwner(released.lease);
released.lease.bindCleanupError(error);
if (!signal?.aborted) return error;
const cancellation = new AnalysisCancelledError(OPERATION, {
cause: signal.reason,
});
const cancellation = cancelledBy(OPERATION, signal);
return actionStatus === "failed"
? error.retainSecondaryFailure(cancellation)
: analysisErrorWithCleanupFailure(
@@ -29,7 +29,10 @@ import {
AnalysisError,
primaryAnalysisError,
} from "../domain/analysisErrorBase.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import {
AnalysisCancelledError,
cancelledBy,
} from "../domain/analysisErrorCore.js";
import { BrowserObservationError } from "../domain/browserObservationError.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { err, ok, type Result } from "../domain/result.js";
@@ -375,7 +378,7 @@ const captureOutcomeWithCancellation = (
return err(
analysisErrorWithCancellation(
outcome.error,
new AnalysisCancelledError(OPERATION, { cause: signal.reason }),
cancelledBy(OPERATION, signal),
),
);
};
+4 -5
View File
@@ -1,7 +1,8 @@
import { MAX_NODE_TIMER_DELAY_MS } from "../domain/analysisResourceLimits.js";
import {
AnalysisCancelledError,
AnalysisTimeoutError,
cancelledBy,
carriedReason,
} from "../domain/analysisErrorCore.js";
export const PLAYWRIGHT_NETWORK_READ_TIMEOUT_MS = 5_000;
@@ -21,9 +22,7 @@ export const withPlaywrightExecutionBoundary = <Value>(
if (timeoutMs !== undefined && timeoutMs <= 0)
return Promise.reject(timeoutError());
if (signal?.aborted === true)
return Promise.reject(
new AnalysisCancelledError(OPERATION, { cause: signal.reason }),
);
return Promise.reject(cancelledBy(OPERATION, signal));
return new Promise<Value>((resolve, reject) => {
let settled = false;
@@ -60,7 +59,7 @@ export const withPlaywrightExecutionBoundary = <Value>(
reject(error);
};
const onAbort = (): void => {
fail(new AnalysisCancelledError(OPERATION, { cause: signal?.reason }));
fail(cancelledBy(OPERATION, carriedReason(signal?.reason)));
};
scheduleTimeout();
if (settled) return;
+15 -26
View File
@@ -38,6 +38,8 @@ import {
AnalysisTimeoutError,
AnalysisOutputError,
AnalysisResourceConstraintError,
cancelledBy,
carriedReason,
} from "../domain/analysisErrorCore.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
@@ -299,17 +301,13 @@ export class WebBundleAnalysisWorker {
): Result<WebBundleScriptAnalysis, WebBundleScriptAnalysisFailure> =>
err({
error: signal?.aborted
? analysisErrorWithCancellation(
error,
new AnalysisCancelledError(OPERATION, { cause: signal.reason }),
)
? analysisErrorWithCancellation(error, cancelledBy(OPERATION, signal))
: error,
completed,
...(continuation === undefined ? {} : { continuation }),
});
try {
if (signal?.aborted)
throw new AnalysisCancelledError(OPERATION, { cause: signal.reason });
if (signal?.aborted) throw cancelledBy(OPERATION, signal);
const admitted = this.#admittedResultBudget();
if (!admitted.ok) {
continuation = "result_capacity_exhausted";
@@ -469,16 +467,15 @@ export class WebBundleAnalysisWorker {
interruptionApplies && deadline?.interruption === "timeout"
? scriptTimeout(timeoutMs, cause)
: cancellationCause !== undefined
? new AnalysisCancelledError(OPERATION, {
cause: cancellationCause.reason,
})
? cancelledBy(OPERATION, carriedReason(cancellationCause.reason))
: interruptionApplies &&
(deadline?.interruption === "cancelled" || signal?.aborted)
? new AnalysisCancelledError(OPERATION, {
cause: signal?.aborted
? signal.reason
: deadline?.signal.reason,
})
? cancelledBy(
OPERATION,
carriedReason(
signal?.aborted ? signal.reason : deadline?.signal.reason,
),
)
: undefined;
const observedFailure =
this.#observedFailure === undefined
@@ -552,17 +549,13 @@ export class WebBundleAnalysisWorker {
const primary = signal?.aborted
? analysisErrorWithCancellation(
previous,
new AnalysisCancelledError(OPERATION, { cause: signal.reason }),
cancelledBy(OPERATION, signal),
)
: previous;
return failed(this.#cleanupFailure(cause, primary));
}
if (signal?.aborted)
return failed(
result.ok
? new AnalysisCancelledError(OPERATION, { cause: signal.reason })
: result.error,
);
return failed(result.ok ? cancelledBy(OPERATION, signal) : result.error);
return result.ok ? result : failed(result.error);
}
@@ -718,9 +711,7 @@ export class WebBundleAnalysisWorker {
if (deadline.interruption === "timeout")
throw scriptTimeout(deadline.timeoutMs);
if (deadline.interruption === "cancelled")
throw new AnalysisCancelledError(OPERATION, {
cause: deadline.signal.reason,
});
throw cancelledBy(OPERATION, deadline.signal);
const frame = this.#frames.shift();
if (frame !== undefined) return frame;
if (this.#runtime.inputFailure !== undefined)
@@ -751,9 +742,7 @@ export class WebBundleAnalysisWorker {
if (completedFrame !== undefined) return completedFrame;
throw deadline.interruption === "timeout"
? scriptTimeout(deadline.timeoutMs)
: new AnalysisCancelledError(OPERATION, {
cause: deadline.signal.reason,
});
: cancelledBy(OPERATION, deadline.signal);
}
const completedFrame = this.#frames.shift();
if (completedFrame !== undefined) return completedFrame;
+37 -64
View File
@@ -6,6 +6,9 @@ import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
import {
AnalysisCancelledError,
AnalysisResourceConstraintError,
isAborted,
cancelledBy,
carriedReason,
} from "../domain/analysisErrorCore.js";
import {
AnalysisError,
@@ -106,12 +109,7 @@ export const fetchWebSourceMaps = async (
signal?: AbortSignal,
host: SourceMapFetchHost = { fetch: globalThis.fetch },
): Promise<Result<WebSourceMaps, AnalysisError>> => {
if (signalIsAborted(signal))
return err(
new AnalysisCancelledError("analyze_web_bundle", {
cause: signal?.reason,
}),
);
if (isAborted(signal)) return err(cancelledBy("analyze_web_bundle", signal));
if (requests.length === 0)
return ok(
webSourceMapsSchema.parse({
@@ -232,13 +230,7 @@ export const fetchWebSourceMaps = async (
signal?.aborted === true &&
(cause === signal.reason || cause === callerAbort)
)
return err(
retainCompleted(
new AnalysisCancelledError("analyze_web_bundle", {
cause: signal.reason,
}),
),
);
return err(retainCompleted(cancelledBy("analyze_web_bundle", signal)));
if (cause instanceof AnalysisError) {
if (
signal?.aborted === true &&
@@ -249,11 +241,7 @@ export const fetchWebSourceMaps = async (
failure.cause === signal.reason,
)
)
cause.retainSecondaryFailure(
new AnalysisCancelledError("analyze_web_bundle", {
cause: signal.reason,
}),
);
cause.retainSecondaryFailure(cancelledBy("analyze_web_bundle", signal));
return err(retainCompleted(cause));
}
if (signal?.aborted === true)
@@ -261,11 +249,7 @@ export const fetchWebSourceMaps = async (
retainCompleted(
new ProviderAdapterError("rea-cdp-browser", "analyze_web_bundle", {
cause,
}).retainSecondaryFailure(
new AnalysisCancelledError("analyze_web_bundle", {
cause: signal.reason,
}),
),
}).retainSecondaryFailure(cancelledBy("analyze_web_bundle", signal)),
),
);
if (isSourceMapLimit(cause))
@@ -279,13 +263,7 @@ export const fetchWebSourceMaps = async (
signal?.removeEventListener("abort", abortFromCaller);
}
if (signal?.aborted === true)
return err(
retainCompleted(
new AnalysisCancelledError("analyze_web_bundle", {
cause: signal.reason,
}),
),
);
return err(retainCompleted(cancelledBy("analyze_web_bundle", signal)));
return ok(
webSourceMapsSchema.parse({
status: sourceMapsStatus(items, limitation !== undefined),
@@ -382,26 +360,22 @@ class SourceMapResponseCleanup extends Error {
super("Source-map response cleanup", { cause: primary });
this.#resource = { url: resource, body };
let reason = "Source-map response cancellation is pending.";
const options = {
cause: sourceMapCallerReason(primary),
cleanup: {
get reason() {
return reason;
},
resources: [resource],
const cause = sourceMapCallerReason(primary);
const cleanup = {
get reason() {
return reason;
},
resources: [resource],
};
this.failure =
signal?.aborted === true &&
primary === signal.reason &&
!(primary instanceof SourceMapDeadlineError)
? new AnalysisCancelledError("analyze_web_bundle", options)
? cancelledBy("analyze_web_bundle", carriedReason(cause), { cleanup })
: new ProviderAdapterError("rea-cdp-browser", "analyze_web_bundle", {
...options,
userMessage:
sourceMapCallerReason(primary) instanceof Error
? (sourceMapCallerReason(primary) as Error).message
: String(sourceMapCallerReason(primary)),
cause,
cleanup,
userMessage: cause instanceof Error ? cause.message : String(cause),
});
this.failure.bindCleanupResourceOwner(this);
const failed = (cause: unknown): void => {
@@ -474,21 +448,21 @@ class SourceMapPendingFetch extends Error {
const reason = signal.reason;
super("Source-map fetch is settling after cancellation", { cause: reason });
this.#resource = { url: resource };
const options = {
cause: sourceMapCallerReason(reason),
cleanup: {
reason: "Source-map fetch is still settling after cancellation.",
resources: [resource],
},
const cleanup = {
reason: "Source-map fetch is still settling after cancellation.",
resources: [resource],
};
this.failure =
reason instanceof SourceMapDeadlineError
? new ProviderAdapterError(
"rea-cdp-browser",
? new ProviderAdapterError("rea-cdp-browser", "analyze_web_bundle", {
cause: reason,
cleanup,
})
: cancelledBy(
"analyze_web_bundle",
options,
)
: new AnalysisCancelledError("analyze_web_bundle", options);
carriedReason(sourceMapCallerReason(reason)),
{ cleanup },
);
this.failure.bindCleanupResourceOwner(this);
const finished = (): void => {
this.#resource = undefined;
@@ -572,9 +546,10 @@ const cancelUnreadResponse = async (
primary !== cause
)
cleanup.failure.retainSecondaryFailure(
new AnalysisCancelledError("analyze_web_bundle", {
cause: sourceMapCallerReason(cause),
}),
cancelledBy(
"analyze_web_bundle",
carriedReason(sourceMapCallerReason(cause)),
),
);
throw cleanup;
}
@@ -803,7 +778,7 @@ const fetchFollowingApprovedRedirects = async (
current,
);
observeResponse(current);
if (signal !== undefined && signalIsAborted(signal)) {
if (signal !== undefined && isAborted(signal)) {
await cancelUnreadResponse(
response,
current,
@@ -911,9 +886,10 @@ const readBoundedText = async (
!(completionFailure instanceof SourceMapDeadlineError)
)
cleanup.failure.retainSecondaryFailure(
new AnalysisCancelledError("analyze_web_bundle", {
cause: sourceMapCallerReason(signal.reason),
}),
cancelledBy(
"analyze_web_bundle",
carriedReason(sourceMapCallerReason(signal.reason)),
),
);
}
}
@@ -978,9 +954,6 @@ const awaitWithAbort = <T>(
});
};
const signalIsAborted = (signal: AbortSignal | undefined): boolean =>
signal?.aborted === true;
const promiseWithAbort = (
promise: Promise<Response>,
signal: AbortSignal | undefined,
+17 -15
View File
@@ -12,7 +12,11 @@ import { SafeOutputTree } from "../../artifacts/SafeOutputTree.js";
import { outputDirectoryAdmissionRefusal } from "../../artifacts/SafeOutputTreeAdmission.js";
import { SafeOutputTreeCreationFailure } from "../../artifacts/SafeOutputTreeCreationFailure.js";
import { PLAYWRIGHT_RESPONSE_BODY_PROVENANCE_LIMITATION } from "../../domain/browserNetworkEvidence.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import {
AnalysisCancelledError,
cancelledBy,
carriedReason,
} from "../../domain/analysisErrorCore.js";
import {
webScriptExportManifestSchema,
webScriptExportResultSchema,
@@ -52,10 +56,7 @@ export const publishWebScripts = async (
): Promise<WebScriptExportResult> =>
options.resources.run(async () => {
const assertActive = (): void => {
if (options.signal?.aborted)
throw new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
});
if (options.signal?.aborted) throw cancelledBy(OPERATION, options.signal);
};
const planned = planWebScriptExport(capture.scripts);
const scenarioResponseBodyCaptured =
@@ -135,9 +136,7 @@ export const publishWebScripts = async (
? cause
: cause instanceof ArtifactReaderFailure &&
cause.cancellation !== undefined
? new AnalysisCancelledError(OPERATION, {
cause: cause.cancellation.reason,
})
? cancelledBy(OPERATION, carriedReason(cause.cancellation.reason))
: undefined;
if (cause instanceof SafeOutputTreeCreationFailure) tree = cause.tree;
if (tree !== undefined) {
@@ -156,13 +155,16 @@ export const publishWebScripts = async (
? ({ kind: "web-script-export", result } as const)
: undefined;
if (cancellation !== undefined) {
const cancelled = new AnalysisCancelledError(OPERATION, {
cause: cancellation.cause,
cleanup: { reason: diagnostic, resources: [tree.outputRoot] },
...(partialObservation === undefined
? {}
: { partialObservation }),
});
const cancelled = cancelledBy(
OPERATION,
carriedReason(cancellation.cause),
{
cleanup: { reason: diagnostic, resources: [tree.outputRoot] },
...(partialObservation === undefined
? {}
: { partialObservation }),
},
);
cleanup.lease.bindCleanupError(cancelled);
throw cancelled;
}
+7 -7
View File
@@ -19,8 +19,9 @@ import {
} from "../../domain/analysisErrorBase.js";
import {
AnalysisAccessDeniedError,
AnalysisCancelledError,
AnalysisInputError,
cancelledBy,
carriedReason,
} from "../../domain/analysisErrorCore.js";
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
@@ -174,9 +175,10 @@ const webArtifactPrimaryError = (
cause instanceof ArtifactReaderFailure &&
cause.reason === "cancelled"
)
return new AnalysisCancelledError(context.operation, {
cause: artifactFileCancellationCause(cause),
});
return cancelledBy(
context.operation,
carriedReason(artifactFileCancellationCause(cause)),
);
if (
origin === "owned-or-classified" &&
signal?.aborted === true &&
@@ -186,9 +188,7 @@ const webArtifactPrimaryError = (
"code" in cause &&
cause.code === "ABORT_ERR"))
)
return new AnalysisCancelledError(context.operation, {
cause: signal.reason,
});
return cancelledBy(context.operation, signal);
const selection = context.callerSelected
? selectedFileError({ cause, origin }, context)
: undefined;
+2 -4
View File
@@ -1,6 +1,6 @@
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import { BrowserObservationError } from "../../domain/browserObservationError.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import { cancelledBy } from "../../domain/analysisErrorCore.js";
import type {
ObserveWebExecutionInput,
WebExecution,
@@ -64,9 +64,7 @@ export const captureWebExecution = async (
const reason = await waiting;
session.beginCompletion();
if (session.options.signal?.aborted)
throw new AnalysisCancelledError(session.operation, {
cause: session.options.signal.reason,
});
throw cancelledBy(session.operation, session.options.signal);
sources.check();
requests.check();
let sampled: CdpCommandResult<"Profiler.takePreciseCoverage"> | undefined;
+2 -4
View File
@@ -1,4 +1,4 @@
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import { cancelledBy, carriedReason } from "../../domain/analysisErrorCore.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type { WebExecution } from "../../domain/webExecution.js";
import type { CdpEvent } from "../CdpConnection.js";
@@ -94,9 +94,7 @@ export class CdpExecutionWindow {
const signal = this.session.options.signal;
const abort = (): void => {
this.fail(
new AnalysisCancelledError(this.session.operation, {
cause: signal?.reason,
}),
cancelledBy(this.session.operation, carriedReason(signal?.reason)),
);
};
this.#removeAbort = () => signal?.removeEventListener("abort", abort);
@@ -6,8 +6,8 @@ import {
type AnalysisPartialObservation,
} from "../../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
cancelledBy,
} from "../../domain/analysisErrorCore.js";
import type { BrowserObservationOperation } from "../../domain/browserObservationErrors.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
@@ -123,8 +123,7 @@ export class CdpWebRuntimeProvider implements WebRuntimePort {
result.ok
? options.signal?.aborted === true
? analysisErrorWithCleanupFailure(
new AnalysisCancelledError(operation, {
cause: options.signal.reason,
cancelledBy(operation, options.signal, {
partialObservation: result.value,
}),
failure,
@@ -137,8 +136,7 @@ export class CdpWebRuntimeProvider implements WebRuntimePort {
const completed = result.ok ? ok(confirmCleanup(result.value)) : result;
if (completed.ok && options.signal?.aborted === true)
return err(
new AnalysisCancelledError(operation, {
cause: options.signal.reason,
cancelledBy(operation, options.signal, {
partialObservation: completed.value,
}),
);
+12 -10
View File
@@ -15,11 +15,12 @@ import { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
AnalysisResourceConstraintError,
AnalysisTimeoutError,
cancelledBy,
carriedReason,
} from "../../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
@@ -138,14 +139,16 @@ export const historicalCaptureFailure = (
: cleanup;
}
if (cause.reason === "cancelled")
return new AnalysisCancelledError(OPERATION, {
cause:
return cancelledBy(
OPERATION,
carriedReason(
cause.cancellation === undefined
? options?.signal?.aborted === true
? options.signal.reason
: undefined
: cause.cancellation.reason,
});
),
);
if (cause.reason === "timeout") {
const timeoutMs = selectedWebNetworkCaptureTimeoutMs(
input.capture_timeout_ms,
@@ -200,9 +203,10 @@ export const historicalCaptureFailure = (
cause instanceof ArtifactReaderFailure &&
cause.reason === "cancelled"
)
return new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
});
return cancelledBy(
OPERATION,
carriedReason(artifactFileCancellationCause(cause)),
);
if (
phase === "decoder" &&
origin === "owned-or-classified" &&
@@ -296,9 +300,7 @@ export const historicalCaptureFailure = (
"code" in cause &&
cause.code === "ABORT_ERR"))
)
return new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
});
return cancelledBy(OPERATION, options.signal);
return new ProviderAdapterError(input.format, OPERATION, {
cause,
diagnostics: {
@@ -46,6 +46,7 @@ import {
AnalysisInputError,
AnalysisOutputError,
AnalysisResourceConstraintError,
cancelledBy,
} from "../../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
@@ -504,11 +505,7 @@ export class HistoricalCaptureDecoder {
}
}
if (result.ok && options?.signal?.aborted)
result = err(
new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
}),
);
result = err(cancelledBy(OPERATION, options.signal));
if (cleanupFailure !== undefined) {
const cleanupError = new ProviderCleanupError(
input.format,
@@ -12,8 +12,9 @@ import {
readStableArtifact,
} from "../../artifacts/readStableArtifact.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
cancelledBy,
carriedReason,
} from "../../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import { withCaptureReaderCleanup } from "./CaptureFailures.js";
@@ -96,9 +97,9 @@ export const readHistoricalCaptureReply = async (
{ cause },
);
if (finalization?.reason === "cancelled") {
const cancellation = new AnalysisCancelledError(
const cancellation = cancelledBy(
"inspect_web_network_capture",
{ cause: artifactFileCancellationCause(finalization) },
carriedReason(artifactFileCancellationCause(finalization)),
);
error.retainSecondaryFailure(cancellation, {
reply_finalization_error: projectAnalysisError(cancellation),
@@ -13,8 +13,8 @@ import { assertArtifactFileSelectedSource } from "../../../artifacts/ArtifactFil
import { hashStableRootArtifactHandle } from "../../../artifacts/inventory/hashStableRootArtifact.js";
import {
AnalysisResourceConstraintError,
AnalysisCancelledError,
AnalysisUnsupportedTargetError,
cancelledBy,
} from "../../../domain/analysisErrorCore.js";
import { fixedAllowanceOptions } from "../../../domain/fixedAllowanceRemediation.js";
import { err, ok } from "../../../domain/result.js";
@@ -209,8 +209,7 @@ class TraceArchiveRead {
reader,
});
if (outcome.kind === "completed" && this.signal?.aborted) {
const cancellation = new AnalysisCancelledError(operation, {
cause: this.signal.reason,
const cancellation = cancelledBy(operation, this.signal, {
partialObservation: this.complete ?? this.identity,
});
this.primary = cancellation;
@@ -232,10 +231,7 @@ export class PlaywrightTraceDecoder implements PlaywrightTracePort {
#running = false;
constructor(readonly resources: ArtifactResourceScope) {}
async inspect(path: string, signal?: AbortSignal) {
if (signal?.aborted)
return err(
new AnalysisCancelledError(operation, { cause: signal.reason }),
);
if (signal?.aborted) return err(cancelledBy(operation, signal));
if (this.#running)
return err(
new AnalysisResourceConstraintError(
@@ -2,9 +2,10 @@ import { AnalysisError } from "../../../domain/analysisErrorBase.js";
import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisInputError,
AnalysisResourceConstraintError,
cancelledBy,
carriedReason,
} from "../../../domain/analysisErrorCore.js";
import { ArtifactOperationError } from "../../../domain/artifactOperationError.js";
import { PLAYWRIGHT_TRACE_OPERATION as operation } from "../../../domain/playwrightTrace/playwrightTrace.js";
@@ -18,9 +19,10 @@ export const traceFailure = (cause: unknown, path: string): AnalysisError => {
if (cause instanceof AnalysisError) return cause;
if (cause instanceof ArtifactReaderFailure) {
if (cause.reason === "cancelled")
return new AnalysisCancelledError(operation, {
cause: artifactFileCancellationCause(cause),
});
return cancelledBy(
operation,
carriedReason(artifactFileCancellationCause(cause)),
);
if (cause.reason === "integrity")
return new AnalysisArtifactChangedError(operation, path, cause.message, {
cause,
+9 -12
View File
@@ -18,10 +18,10 @@ import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
import { analysisErrorWithCancellation } from "../../domain/analysisErrorCancellation.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
AnalysisTimeoutError,
cancelledBy,
} from "../../domain/analysisErrorCore.js";
import { BrowserObservationError } from "../../domain/browserObservationError.js";
import { jsonObjectSchema } from "../../domain/jsonValue.js";
@@ -128,9 +128,7 @@ export class NativeModuleResolver implements WebModuleResolutionPort {
options?: ExecutionOptions,
): ReturnType<WebModuleResolutionPort["resolve"]> {
if (options?.signal?.aborted === true)
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
return err(cancelledBy(OPERATION, options.signal));
const pending = await this.#retryCleanup();
if (pending !== undefined) return err(pending);
const executable = this.environment.REA_BROWSER_EXECUTABLE;
@@ -159,9 +157,7 @@ export class NativeModuleResolver implements WebModuleResolutionPort {
let interruption: AnalysisError | undefined;
const interrupted = (): AnalysisError =>
deadline.interruption === "cancelled"
? new AnalysisCancelledError(OPERATION, {
cause: deadline.signal.reason,
})
? cancelledBy(OPERATION, deadline.signal)
: new AnalysisTimeoutError(OPERATION, TIMEOUT_MS);
const assertActive = (): void => {
if (!deadline.signal.aborted) return;
@@ -288,14 +284,15 @@ export class NativeModuleResolver implements WebModuleResolutionPort {
result = err(
analysisErrorWithCancellation(
result.ok ? undefined : result.error,
new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
...(result.ok
cancelledBy(
OPERATION,
options.signal,
result.ok
? {
partialObservation: completedObservation(input, result.value),
}
: {}),
}),
: {},
),
),
);
if (cleanupError !== undefined)
+8 -10
View File
@@ -51,10 +51,11 @@ import {
} from "../domain/analysisErrorBase.js";
import {
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
AnalysisResourceConstraintError,
cancelledBy,
carriedReason,
} from "../domain/analysisErrorCore.js";
import { createAnalysisExecutionEvidence } from "../domain/analysisObservation.js";
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
@@ -153,9 +154,7 @@ export class TsharkCaptureProvider implements PacketCapturePort {
let result: Result<AnalysisExecution, AnalysisError>;
try {
if (options?.signal?.aborted)
throw new AnalysisCancelledError(operation, {
cause: options.signal.reason,
});
throw cancelledBy(operation, options.signal);
await this.#resources.release();
if (!tsharkHostSupported(this.platform))
throw new Error(
@@ -527,12 +526,11 @@ export class TsharkCaptureProvider implements PacketCapturePort {
operation,
);
if (cause instanceof ArtifactReaderFailure && cause.reason === "cancelled")
return new AnalysisCancelledError(operation, {
cause: artifactFileCancellationCause(cause),
...(observedOutput === undefined
? {}
: { capturedOutput: observedOutput }),
});
return cancelledBy(
operation,
carriedReason(artifactFileCancellationCause(cause)),
observedOutput === undefined ? {} : { capturedOutput: observedOutput },
);
if (cause instanceof FieldDiscoveryBudgetError)
return new AnalysisResourceConstraintError(
operation,
+8 -10
View File
@@ -14,10 +14,11 @@ import { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
AnalysisResourceConstraintError,
cancelledBy,
carriedReason,
} from "../domain/analysisErrorCore.js";
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
@@ -54,9 +55,7 @@ export const electronFuseFailure = (
? electronFuseFailure(outcome.cause, path, operation)
: cancellation === undefined
? undefined
: new AnalysisCancelledError(operation, {
cause: cancellation.reason,
});
: cancelledBy(operation, carriedReason(cancellation.reason));
if (primary !== undefined)
return analysisErrorWithCleanupFailure(
primary,
@@ -67,14 +66,13 @@ export const electronFuseFailure = (
return cleanup;
}
if (cause instanceof ArtifactReaderFailure && cause.reason === "cancelled")
return new AnalysisCancelledError(operation, {
cause: artifactFileCancellationCause(cause),
});
return cancelledBy(
operation,
carriedReason(artifactFileCancellationCause(cause)),
);
const cancellation = regularFileCancellation(cause);
if (cancellation !== undefined)
return new AnalysisCancelledError(operation, {
cause: cancellation.reason,
});
return cancelledBy(operation, carriedReason(cancellation.reason));
if (cause instanceof ElectronFusePathFailure)
return new AnalysisInputError(operation, { cause }, [
{ path: ["path"], reason: cause.reason, message: cause.message },
+2 -3
View File
@@ -13,7 +13,7 @@ import type {
JavaScriptRuntimeLocation,
JavaScriptRuntimeTargetLocation,
} from "../domain/javascript/javascriptRuntimeObservation.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { cancelledBy } from "../domain/analysisErrorCore.js";
import {
authorizedElectronFile,
type ElectronFileScope,
@@ -33,8 +33,7 @@ export const throwIfRuntimeObservationCancelled = (
| "observe_javascript_runtime"
| "list_javascript_runtime_targets" = "observe_javascript_runtime",
): void => {
if (signal?.aborted === true)
throw new AnalysisCancelledError(operation, { cause: signal.reason });
if (signal?.aborted === true) throw cancelledBy(operation, signal);
};
/**
+29 -26
View File
@@ -19,6 +19,8 @@ import {
import { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
cancelledBy,
carriedReason,
OwnerCancellation,
} from "../domain/analysisErrorCore.js";
import { BrowserObservationError } from "../domain/browserObservationError.js";
@@ -316,29 +318,32 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort {
),
];
return err(
new AnalysisCancelledError("list_javascript_runtime_targets", {
cause: cause.cause,
diagnostics: {
completed_target_inventory: {
runtime: discovery.runtime,
targets: allowed
.sort((left, right) =>
left.id < right.id ? -1 : left.id > right.id ? 1 : 0,
)
.map(projectTarget),
excluded: { ...excluded, unconnectable },
targets_examined: targetsExamined,
targets_discovered: discovery.targets.length,
complete,
limitations: complete
? limitations
: [
...limitations,
"Cancellation stopped target authorization; unexamined targets are unknown, not excluded.",
],
cancelledBy(
"list_javascript_runtime_targets",
carriedReason(cause.cause),
{
diagnostics: {
completed_target_inventory: {
runtime: discovery.runtime,
targets: allowed
.sort((left, right) =>
left.id < right.id ? -1 : left.id > right.id ? 1 : 0,
)
.map(projectTarget),
excluded: { ...excluded, unconnectable },
targets_examined: targetsExamined,
targets_discovered: discovery.targets.length,
complete,
limitations: complete
? limitations
: [
...limitations,
"Cancellation stopped target authorization; unexamined targets are unknown, not excluded.",
],
},
},
},
}),
),
);
}
return err(providerError(cause, "list_javascript_runtime_targets"));
@@ -479,12 +484,10 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort {
// waiting for its bounded transport close. Preserve it as the primary
// result even when cleanup completed normally.
if (options.signal?.aborted === true && outcome?.ok === true) {
const cancellation = new AnalysisCancelledError(
const cancellation = cancelledBy(
"observe_javascript_runtime",
{
cause: options.signal.reason,
partialObservation: outcome.value,
},
options.signal,
{ partialObservation: outcome.value },
);
outcome = err(cancellation);
}
@@ -40,6 +40,7 @@ import {
AnalysisTimeoutError,
AnalysisOutputError,
AnalysisResourceConstraintError,
cancelledBy,
} from "../../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
@@ -332,8 +333,7 @@ export class JavaScriptAnalysisWorker {
JavaScriptSourceAnalysisFailure
>;
try {
if (signal?.aborted === true)
throw new AnalysisCancelledError(OPERATION, { cause: signal.reason });
if (signal?.aborted === true) throw cancelledBy(OPERATION, signal);
if (!file.text.included || file.kind !== "javascript")
throw new TypeError(
"JavaScript analysis worker requires included JavaScript text",
@@ -566,8 +566,7 @@ export class JavaScriptAnalysisWorker {
if (deadline?.interruption === "cancelled")
analysisErrorWithCancellation(
error,
new AnalysisCancelledError(OPERATION, {
cause: deadline.signal.reason,
cancelledBy(OPERATION, deadline.signal, {
capturedOutput: this.#captured(),
}),
);
@@ -616,8 +615,7 @@ export class JavaScriptAnalysisWorker {
const primary = signal?.aborted
? analysisErrorWithCancellation(
previous,
new AnalysisCancelledError(OPERATION, {
cause: signal.reason,
cancelledBy(OPERATION, signal, {
capturedOutput: this.#captured(),
}),
)
@@ -635,10 +633,7 @@ export class JavaScriptAnalysisWorker {
if (signal?.aborted === true) {
const error = analysisErrorWithCancellation(
result.ok ? undefined : result.error.error,
new AnalysisCancelledError(OPERATION, {
cause: signal.reason,
capturedOutput: this.#captured(),
}),
cancelledBy(OPERATION, signal, { capturedOutput: this.#captured() }),
);
if (result.ok) this.#lastFailure = error;
result = err({ error, javascript, module, projection });
@@ -1221,8 +1216,7 @@ export class JavaScriptAnalysisWorker {
remediationAction:
"Reuse accessible partial_observation Evidence to avoid recomputation; reading it is not a prerequisite for a corrected retry. Increase analysis_timeout_ms through MCP, or use rea analyze-javascript-application <input_path> --analysis-timeout-ms <milliseconds> on the CLI. Alternatively, reduce semantic limits or select exact inventoried source_paths (CLI: --source-path on that command). Omit source_paths to analyze all sources; an empty array is invalid. detail: summary reduces response size, not analysis work. Allow execution, cleanup and delivery headroom in the MCP client tool deadline; increasing that client deadline alone does not change this server budget. Resolve any reported incomplete cleanup before retrying.",
})
: new AnalysisCancelledError(OPERATION, {
cause: deadline.signal.reason,
: cancelledBy(OPERATION, deadline.signal, {
capturedOutput: this.#captured(),
});
}
+19 -14
View File
@@ -28,6 +28,8 @@ import {
AnalysisOutputError,
AnalysisResourceConstraintError,
AnalysisTimeoutError,
cancelledBy,
carriedReason,
} from "../../domain/analysisErrorCore.js";
import {
NonRegularFileReadError,
@@ -109,19 +111,23 @@ const scanRecoveryFile = async (
} catch (cause: unknown) {
if (cause instanceof ArtifactFileCleanupFailure) throw cause;
if (cause instanceof ArtifactFileCancelled)
throw new AnalysisCancelledError(OPERATION, {
cause: cause.fileCancellation.reason,
partialObservation: fileSnapshotPartialObservationSchema.parse(
Object.assign({}, cause.fileCancellation.outcome.value, {
kind: "file-snapshot-hash",
path,
}),
),
});
throw cancelledBy(
OPERATION,
carriedReason(cause.fileCancellation.reason),
{
partialObservation: fileSnapshotPartialObservationSchema.parse(
Object.assign({}, cause.fileCancellation.outcome.value, {
kind: "file-snapshot-hash",
path,
}),
),
},
);
if (cause instanceof ArtifactReaderFailure && cause.reason === "cancelled")
throw new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
});
throw cancelledBy(
OPERATION,
carriedReason(artifactFileCancellationCause(cause)),
);
if (
cause instanceof Error &&
"code" in cause &&
@@ -482,8 +488,7 @@ export const recoveryFailureMessage = (cause: unknown): string =>
/** Cancellation remains distinct from malformed source or provider output. */
export const checkRecoveryCancellation = (signal?: AbortSignal): void => {
if (signal?.aborted === true)
throw new AnalysisCancelledError(OPERATION, { cause: signal.reason });
if (signal?.aborted === true) throw cancelledBy(OPERATION, signal);
};
/** Keep both tool invocations within one operation's execution budget. */
+3 -6
View File
@@ -8,6 +8,7 @@ import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
cancelledBy,
} from "../../domain/analysisErrorCore.js";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
@@ -200,9 +201,7 @@ export const runWakaruCommand = async (context: WakaruCommandContext) => {
context.signal?.aborted === true
? analysisErrorWithCancellation(
physical,
new AnalysisCancelledError(OPERATION, {
cause: context.signal.reason,
}),
cancelledBy(OPERATION, context.signal),
)
: physical;
}
@@ -269,9 +268,7 @@ export const runWakaruCommand = async (context: WakaruCommandContext) => {
const stopped = await supervisor.stop();
const snapshot = supervisor.snapshot();
if (context.signal?.aborted === true) {
const cancelled = new AnalysisCancelledError(OPERATION, {
cause: context.signal.reason,
});
const cancelled = cancelledBy(OPERATION, context.signal);
if (failure === undefined || failure instanceof AnalysisError)
failure = analysisErrorWithCancellation(failure, cancelled);
}
+15 -11
View File
@@ -14,9 +14,10 @@ import { SafeOutputTreeCreationFailure } from "../../artifacts/SafeOutputTreeCre
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import { analysisErrorWithCancellation } from "../../domain/analysisErrorCancellation.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
cancelledBy,
carriedReason,
} from "../../domain/analysisErrorCore.js";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { artifactFileCancellationCause } from "../../artifacts/ArtifactFileOperation.js";
@@ -167,15 +168,18 @@ export class WakaruProvider implements JavaScriptRecoveryPort {
primary instanceof ArtifactReaderFailure &&
primary.reason === "cancelled" &&
primary.cleanup === undefined
? new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(primary),
...(primary.partialObservation === undefined
? {}
: { partialObservation: primary.partialObservation }),
...(reportFailure === undefined
? {}
: { capturedOutput: reportFailure.capturedOutput }),
})
? cancelledBy(
OPERATION,
carriedReason(artifactFileCancellationCause(primary)),
{
...(primary.partialObservation === undefined
? {}
: { partialObservation: primary.partialObservation }),
...(reportFailure === undefined
? {}
: { capturedOutput: reportFailure.capturedOutput }),
},
)
: primary;
const residuals: string[] = [];
const cleanupFailures: { resource: string; reason: string }[] = [];
@@ -269,7 +273,7 @@ export class WakaruProvider implements JavaScriptRecoveryPort {
});
failure = analysisErrorWithCancellation(
analysisFailure,
new AnalysisCancelledError(OPERATION, { cause: signal.reason }),
cancelledBy(OPERATION, signal),
);
}
if (residuals.length > 0) {
+6 -10
View File
@@ -13,10 +13,11 @@ import { AnalysisError } from "../../domain/analysisErrorBase.js";
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
import { analysisErrorWithCancellation } from "../../domain/analysisErrorCancellation.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
AnalysisResourceConstraintError,
AnalysisTimeoutError,
cancelledBy,
carriedReason,
} from "../../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
@@ -148,7 +149,7 @@ export const runSourceMapCommand = async (
const primary = signal?.aborted
? analysisErrorWithCancellation(
knownPhysical ? physical : undefined,
new AnalysisCancelledError(OPERATION, { cause: signal.reason }),
cancelledBy(OPERATION, signal),
)
: physical;
if (preparation?.cleanupFailure === undefined) return err(primary);
@@ -194,8 +195,7 @@ export const runSourceMapCommand = async (
? err(
analysisErrorWithCancellation(
result.ok ? undefined : result.error,
new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
cancelledBy(OPERATION, options.signal, {
partialObservation: observation,
}),
),
@@ -394,9 +394,7 @@ class OwnedCodec implements SourceMapCleanupOwner {
if (outcome?.kind === "physical")
throw this.#physicalFailure(outcome.observation);
if (outcome?.kind === "cancelled")
throw new AnalysisCancelledError(OPERATION, {
cause: outcome.callerReason,
});
throw cancelledBy(OPERATION, carriedReason(outcome.callerReason));
if (outcome?.kind === "timeout")
throw new AnalysisTimeoutError(
OPERATION,
@@ -411,9 +409,7 @@ class OwnedCodec implements SourceMapCleanupOwner {
if (outcome?.kind === "physical")
return this.#physicalFailure(outcome.observation);
if (outcome?.kind === "cancelled")
return new AnalysisCancelledError(OPERATION, {
cause: outcome.callerReason,
});
return cancelledBy(OPERATION, carriedReason(outcome.callerReason));
if (outcome?.kind === "timeout")
return new AnalysisTimeoutError(
OPERATION,
+4 -10
View File
@@ -8,9 +8,9 @@ import type { WebSourceMapPort } from "../../application/WebSourceLocationPorts.
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { analysisErrorWithCancellation } from "../../domain/analysisErrorCancellation.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisInputError,
cancelledBy,
} from "../../domain/analysisErrorCore.js";
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
import { err, type Result } from "../../domain/result.js";
@@ -79,16 +79,12 @@ export class SourceMapDecoder implements WebSourceMapPort {
options?.signal?.aborted
? analysisErrorWithCancellation(
cleanupFailure,
new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
}),
cancelledBy(OPERATION, options.signal),
)
: cleanupFailure,
);
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
return err(cancelledBy(OPERATION, options.signal));
const request = sourceMapCodecInputSchema.safeParse({
text: input.text,
url: input.url,
@@ -125,9 +121,7 @@ export class SourceMapDecoder implements WebSourceMapPort {
encodedBytes += bytes;
}
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
return err(cancelledBy(OPERATION, options.signal));
const serialized = encoded.join("");
const retainedCleanup: SourceMapCleanupRetention = {};
const response = await runSourceMapCommand(