Resume patch downloads that die mid-body instead of starting over (#82)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jared Schoeny
2026-08-28 10:51:49 -06:00
committed by GitHub
parent 567f79c00e
commit 22ebbb78d0
5 changed files with 302 additions and 22 deletions

View File

@@ -1132,6 +1132,8 @@ export interface PatchDownloadEventInput {
pageOrigin?: string | null;
correlationId?: string | null;
sampleRate?: number | null;
resumeCount?: number | null;
receivedBytes?: number | null;
}
const PATCH_DOWNLOAD_STAGES = new Set(["signed_url", "fetch", "patch"]);
@@ -1255,6 +1257,8 @@ export async function reportPatchDownloadEvent(
page_origin: sanitizePageOrigin(input.pageOrigin),
correlation_id: sanitizeCorrelationId(input.correlationId),
sample_rate: sanitizeSampleRate(input.sampleRate),
resume_count: sanitizeNonNegativeInteger(input.resumeCount),
received_bytes: sanitizeNonNegativeInteger(input.receivedBytes),
});
if (error) {

View File

@@ -16,12 +16,11 @@ import {
import { getOrCreateDeviceId } from "@/utils/deviceId";
import {
collectFetchDiagnostics,
collectResponseMetadata,
HTTPError,
runConnectivityProbes,
type FetchFailurePhase,
type ResponseMetadata,
} from "@/utils/patches/download-telemetry";
import { downloadPatch, DownloadPatchError } from "@/utils/patches/download-patch";
import type { SelectablePatch } from "@/types/patcher";
import { applyPatch, patchFormatFromFilename, type PatchFormat } from "@/utils/patching";
import { createOutputSink, SaveCancelledError, type OutputSink } from "@/utils/patching/save";
@@ -86,7 +85,9 @@ const HackActions: React.FC<HackActionsProps> = ({
const [romErrorModal, setRomErrorModal] = React.useState<BaseRomErrorModalState | null>(null);
const [isVerifyingRom, setIsVerifyingRom] = React.useState(false);
const [selectedPatchId, setSelectedPatchId] = React.useState<number | null>(patcherSelector.defaultPatchId);
const selectedPatchIdRef = React.useRef(selectedPatchId);
const fetchSessionIdRef = React.useRef<string | null | undefined>(undefined);
selectedPatchIdRef.current = selectedPatchId;
function getFetchSessionId(): string | null {
if (fetchSessionIdRef.current !== undefined) {
@@ -128,6 +129,7 @@ const HackActions: React.FC<HackActionsProps> = ({
function onVersionChange(nextPatchId: number) {
if (nextPatchId === selectedPatchId) return;
selectedPatchIdRef.current = nextPatchId;
setSelectedPatchId(nextPatchId);
resetPatchSession();
}
@@ -248,8 +250,10 @@ const HackActions: React.FC<HackActionsProps> = ({
}
async function onAgreeToTerms(): Promise<{ url: string; blob: Blob; format: PatchFormat } | null> {
const selectedPatchIdAtStart = selectedPatchId;
const eventPatchId = selectedPatch?.id ?? patchId ?? null;
const online = typeof navigator !== "undefined" ? navigator.onLine : null;
const correlationId = getFetchSessionId();
let signedUrl: string;
let signedFormat: PatchFormat;
@@ -273,12 +277,15 @@ const HackActions: React.FC<HackActionsProps> = ({
errorMessage: result.error,
online,
pageOrigin: getPageOrigin(),
correlationId: getFetchSessionId(),
correlationId,
probeSameOrigin: "skipped",
probePatchHost: "skipped",
});
return null;
}
if (selectedPatchIdRef.current !== selectedPatchIdAtStart) {
return null;
}
signedUrl = result.url;
signedFormat = result.format;
} catch (e: any) {
@@ -293,13 +300,17 @@ const HackActions: React.FC<HackActionsProps> = ({
errorMessage: e?.message ?? null,
online,
pageOrigin: getPageOrigin(),
correlationId: getFetchSessionId(),
correlationId,
probeSameOrigin: "skipped",
probePatchHost: "skipped",
});
return null;
}
if (selectedPatchIdRef.current !== selectedPatchIdAtStart) {
return null;
}
setPatchUrl(signedUrl);
setPatchFormat(signedFormat);
setTermsAgreed(true);
@@ -314,50 +325,76 @@ const HackActions: React.FC<HackActionsProps> = ({
};
const sessionFields = {
pageOrigin: getPageOrigin(),
correlationId: getFetchSessionId(),
correlationId,
};
try {
const res = await fetch(signedUrl);
failurePhase = "response";
responseMeta = collectResponseMetadata(res);
if (!res.ok) throw new HTTPError(res.status);
failurePhase = "body";
const blob = await res.blob();
setPatchBlob(blob);
const download = await downloadPatch({
url: signedUrl,
getNewSignedUrl: async () => {
const result = await getSignedPatchUrl(
hackSlug,
eventPatchId == null ? undefined : { patchId: eventPatchId },
);
if (!result.ok) throw new Error(result.error);
return result.url;
},
});
signedUrl = download.url;
failurePhase = download.failurePhase;
responseMeta = download.responseMetadata;
const selectionIsCurrent = selectedPatchIdRef.current === selectedPatchIdAtStart;
if (selectionIsCurrent) {
setPatchUrl(download.url);
setPatchBlob(download.blob);
}
const romReady = isRomReadyForPatch();
if (!romReady) {
if (selectionIsCurrent && !romReady) {
setStatus("idle");
}
if (Math.random() < 0.1) {
const shouldReportSuccess = download.resumeCount > 0 || Math.random() < 0.1;
if (shouldReportSuccess) {
const elapsed = performance.now() - fetchStartedAt;
const diagnostics = collectFetchDiagnostics(signedUrl, elapsed);
const diagnostics = collectFetchDiagnostics(download.url, elapsed);
deferReport({
patchId: eventPatchId,
hackSlug,
stage: "fetch",
outcome: "success",
online,
sampleRate: 0.1,
sampleRate: download.resumeCount > 0 ? 1 : 0.1,
resumeCount: download.resumeCount,
receivedBytes: download.receivedBytes,
...sessionFields,
...responseMeta,
...diagnostics,
});
}
return { url: signedUrl, blob, format: signedFormat };
if (!selectionIsCurrent) return null;
return { url: download.url, blob: download.blob, format: signedFormat };
} catch (e: any) {
setError(e?.name === "HTTPError" ? "Failed to fetch patch" : (e?.message || "Failed to fetch patch URL"));
setStatus("idle");
setTermsAgreed(false);
const downloadError = e instanceof DownloadPatchError ? e : null;
const failureUrl = downloadError?.url ?? signedUrl;
const resumeCount = downloadError?.resumeCount ?? 0;
const receivedBytes = downloadError?.receivedBytes ?? 0;
failurePhase = downloadError?.failurePhase ?? failurePhase;
responseMeta = downloadError?.responseMetadata ?? responseMeta;
if (selectedPatchIdRef.current === selectedPatchIdAtStart) {
setError(e?.name === "HTTPError" ? "Failed to fetch patch" : (e?.message || "Failed to fetch patch URL"));
setStatus("idle");
setTermsAgreed(false);
}
const elapsed = performance.now() - fetchStartedAt;
setTimeout(async () => {
try {
const probes = await runConnectivityProbes(signedUrl);
const diagnostics = collectFetchDiagnostics(signedUrl, elapsed);
const probes = await runConnectivityProbes(failureUrl);
const diagnostics = collectFetchDiagnostics(failureUrl, elapsed);
await reportPatchDownloadEvent({
patchId: eventPatchId,
hackSlug,
@@ -367,6 +404,8 @@ const HackActions: React.FC<HackActionsProps> = ({
errorName: e?.name ?? null,
errorMessage: e?.message ?? null,
online,
resumeCount,
receivedBytes,
...sessionFields,
...responseMeta,
...diagnostics,

View File

@@ -368,7 +368,9 @@ export type Database = {
patch: number | null
probe_patch_host: string | null
probe_same_origin: string | null
received_bytes: number | null
response_status: number | null
resume_count: number | null
sample_rate: number | null
stage: string
timing_entry_present: boolean | null
@@ -397,7 +399,9 @@ export type Database = {
patch?: number | null
probe_patch_host?: string | null
probe_same_origin?: string | null
received_bytes?: number | null
response_status?: number | null
resume_count?: number | null
sample_rate?: number | null
stage: string
timing_entry_present?: boolean | null
@@ -426,7 +430,9 @@ export type Database = {
patch?: number | null
probe_patch_host?: string | null
probe_same_origin?: string | null
received_bytes?: number | null
response_status?: number | null
resume_count?: number | null
sample_rate?: number | null
stage?: string
timing_entry_present?: boolean | null

View File

@@ -0,0 +1,224 @@
import {
collectResponseMetadata,
HTTPError,
type FetchFailurePhase,
type ResponseMetadata,
} from "@/utils/patches/download-telemetry";
const MAX_ATTEMPTS = 5;
type DownloadDetails = {
responseMetadata: ResponseMetadata;
failurePhase: FetchFailurePhase;
resumeCount: number;
receivedBytes: number;
url: string;
};
export type DownloadPatchResult = DownloadDetails & {
blob: Blob;
};
export class DownloadPatchError extends Error implements DownloadDetails {
readonly responseMetadata: ResponseMetadata;
readonly failurePhase: FetchFailurePhase;
readonly resumeCount: number;
readonly receivedBytes: number;
readonly url: string;
constructor(error: unknown, details: DownloadDetails) {
const message = error instanceof Error ? error.message : "Failed to fetch patch";
super(message, { cause: error });
this.name = error instanceof Error ? error.name : "DownloadPatchError";
this.responseMetadata = details.responseMetadata;
this.failurePhase = details.failurePhase;
this.resumeCount = details.resumeCount;
this.receivedBytes = details.receivedBytes;
this.url = details.url;
}
}
export type DownloadPatchOptions = {
url: string;
getNewSignedUrl: () => Promise<string>;
};
const EMPTY_RESPONSE_METADATA: ResponseMetadata = {
responseStatus: null,
contentLength: null,
contentEncoding: null,
contentType: null,
};
function parseContentRange(value: string | null): { start: number; total: number } | null {
if (value == null) return null;
const match = /^bytes (\d+)-\d+\/(\d+)$/.exec(value.trim());
if (!match) return null;
const start = Number(match[1]);
const total = Number(match[2]);
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(total)) return null;
return { start, total };
}
export async function downloadPatch({
url: initialUrl,
getNewSignedUrl,
}: DownloadPatchOptions): Promise<DownloadPatchResult> {
let url = initialUrl;
let chunks: BlobPart[] = [];
let receivedBytes = 0;
let expectedLength: number | null = null;
let canResume = false;
let shouldResume = false;
let resumeCount = 0;
let responseMetadata = EMPTY_RESPONSE_METADATA;
let failurePhase: FetchFailurePhase = "request";
let lastError: unknown = new Error("Failed to fetch patch");
const fail = (error: unknown): DownloadPatchError =>
new DownloadPatchError(error, {
responseMetadata,
failurePhase,
resumeCount,
receivedBytes,
url,
});
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
const rangeStart = shouldResume ? receivedBytes : null;
if (rangeStart != null) resumeCount += 1;
let response: Response;
failurePhase = "request";
try {
response = await fetch(url, rangeStart == null
? undefined
: { headers: { Range: `bytes=${rangeStart}-` } });
} catch (error) {
lastError = error;
chunks = [];
receivedBytes = 0;
expectedLength = null;
canResume = false;
shouldResume = false;
continue;
}
failurePhase = "response";
responseMetadata = collectResponseMetadata(response);
if (response.status === 403) {
chunks = [];
receivedBytes = 0;
expectedLength = null;
canResume = false;
shouldResume = false;
try {
failurePhase = "request";
url = await getNewSignedUrl();
} catch (error) {
throw fail(error);
}
continue;
}
if (!response.ok) {
throw fail(new HTTPError(response.status));
}
if (rangeStart != null && response.status === 206) {
const contentRange = parseContentRange(response.headers.get("content-range"));
if (
contentRange == null
|| contentRange.start !== rangeStart
|| contentRange.total !== expectedLength
) {
chunks = [];
receivedBytes = 0;
expectedLength = null;
canResume = false;
shouldResume = false;
lastError = new Error("Invalid Content-Range");
continue;
}
} else if (rangeStart != null && response.status === 200) {
chunks = [];
receivedBytes = 0;
expectedLength = responseMetadata.contentLength;
canResume = expectedLength != null && responseMetadata.contentEncoding == null;
shouldResume = false;
} else if (rangeStart == null) {
expectedLength = responseMetadata.contentLength;
canResume = expectedLength != null && responseMetadata.contentEncoding == null;
} else {
throw fail(new HTTPError(response.status));
}
const bytesBeforeAttempt = receivedBytes;
let bodyFailed = false;
failurePhase = "body";
if (!response.body) {
try {
const blob = await response.blob();
chunks.push(blob);
receivedBytes += blob.size;
} catch (error) {
throw fail(error);
}
} else {
const reader = response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(new Uint8Array(value));
receivedBytes += value.byteLength;
}
} catch (error) {
bodyFailed = true;
lastError = error;
if (expectedLength != null && receivedBytes === expectedLength) {
return {
blob: new Blob(chunks),
responseMetadata,
failurePhase,
resumeCount,
receivedBytes,
url,
};
}
}
}
if (!bodyFailed && (expectedLength == null || receivedBytes === expectedLength)) {
return {
blob: new Blob(chunks),
responseMetadata,
failurePhase,
resumeCount,
receivedBytes,
url,
};
}
if (receivedBytes === bytesBeforeAttempt) {
throw fail(lastError);
}
if (
canResume
&& expectedLength != null
&& receivedBytes > 0
&& receivedBytes < expectedLength
) {
shouldResume = true;
continue;
}
throw fail(lastError);
}
throw fail(lastError);
}

View File

@@ -0,0 +1,7 @@
-- Additive resume diagnostics for patch download telemetry.
alter table public.patch_download_events
add column if not exists resume_count integer
check (resume_count is null or resume_count >= 0),
add column if not exists received_bytes bigint
check (received_bytes is null or received_bytes >= 0);