feat(desktop): show download progress and diagnostics on splash (#3273)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-08 03:06:06 +02:00
1 parent 795e190bda
commit 0ef1cd14c3
12 files changed
+667 -18

No files matched your search

@@ -14,7 +14,8 @@ import type {ModulePlanItem} from '@electron/main/ModuleUpdatePlanner';
const MODULE_PACKAGE_HASH_MISMATCH_RETRIES = 1;
const MODULE_PACKAGE_HEADER_TIMEOUT_MS = 30000;
const MODULE_PACKAGE_STALL_TIMEOUT_MS = 60000;
export const MODULE_PACKAGE_STALL_TIMEOUT_MS = 30000;
const MODULE_PACKAGE_PROGRESS_INTERVAL_MS = 250;
const TRANSIENT_HTTP_STATUS_FLOOR = 500;
const TRANSIENT_PACKAGE_HTTP_STATUSES: ReadonlySet<number> = new Set([408, 425, 429]);
const CAUSE_CHAIN_LIMIT = 8;
@@ -37,6 +38,8 @@ interface ModulePackageInstallProgress {
readonly total: number;
readonly moduleName: string;
readonly progress: number;
readonly receivedBytes: number;
readonly totalBytes: number;
}
export const ModulePackageInstallerReportType = Object.freeze({
@@ -77,6 +80,7 @@ interface ModulePackageInstallerOptions {
readonly packageOrigin: string;
readonly fetch: typeof globalThis.fetch;
readonly onProgress: (progress: ModulePackageInstallProgress) => void;
readonly now?: () => number;
readonly report: (report: ModulePackageInstallerReport) => void;
}
@@ -92,6 +96,17 @@ export class ModulePackageFetchError extends Error {
}
}
export class ModulePackageStallError extends ModulePackageFetchError {
public constructor(moduleName: string, stallMs: number) {
super(moduleName, null, `package download stalled for ${stallMs}ms`);
this.name = 'ModulePackageStallError';
}
}
export function isModulePackageStall(error: unknown): boolean {
return findCause(error, ModulePackageStallError) != null;
}
class ModulePackageUrlPolicyError extends Error {
public readonly module: string;
@@ -168,6 +183,7 @@ export class ModulePackageInstaller {
private readonly packageOrigin: string;
private readonly fetchImplementation: typeof globalThis.fetch;
private readonly onProgress: (progress: ModulePackageInstallProgress) => void;
private readonly now: () => number;
private readonly reporter: (report: ModulePackageInstallerReport) => void;
public constructor(options: ModulePackageInstallerOptions) {
@@ -175,6 +191,7 @@ export class ModulePackageInstaller {
this.packageOrigin = options.packageOrigin;
this.fetchImplementation = options.fetch;
this.onProgress = options.onProgress;
this.now = options.now ?? Date.now;
this.reporter = options.report;
}
@@ -192,6 +209,8 @@ export class ModulePackageInstaller {
total,
moduleName: item.module,
progress: Math.round((current / total) * 100),
receivedBytes: item.entry.bytes,
totalBytes: item.entry.bytes,
});
return;
} catch (error) {
@@ -238,6 +257,8 @@ export class ModulePackageInstaller {
total,
moduleName: item.module,
progress: item.entry.bytes === 0 ? 0 : Math.floor((offset / item.entry.bytes) * 100),
receivedBytes: offset,
totalBytes: item.entry.bytes,
});
if (offset > 0 && offset === item.entry.bytes) {
return {offset, chunks: []};
@@ -339,18 +360,13 @@ export class ModulePackageInstaller {
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
let received = start;
let reported = item.entry.bytes === 0 ? 0 : Math.floor((start / item.entry.bytes) * 100);
let reportedAt = this.now();
let completed = false;
let transferFailure: unknown = null;
let stall: NodeJS.Timeout | null = null;
const arm = (): void => {
stall = setTimeout(() => {
controller.abort(
new ModulePackageFetchError(
item.module,
null,
`package download stalled for ${MODULE_PACKAGE_STALL_TIMEOUT_MS}ms`,
),
);
controller.abort(new ModulePackageStallError(item.module, MODULE_PACKAGE_STALL_TIMEOUT_MS));
}, MODULE_PACKAGE_STALL_TIMEOUT_MS);
};
const disarm = (): void => {
@@ -378,14 +394,18 @@ export class ModulePackageInstaller {
);
}
const percent = item.entry.bytes === 0 ? 100 : Math.floor((received / item.entry.bytes) * 100);
if (percent > reported) {
const at = this.now();
if (percent > reported || at - reportedAt >= MODULE_PACKAGE_PROGRESS_INTERVAL_MS) {
reported = percent;
reportedAt = at;
this.onProgress({
phase: ModulePackageInstallPhase.DOWNLOADING,
current,
total,
moduleName: item.module,
progress: percent,
receivedBytes: received,
totalBytes: item.entry.bytes,
});
}
yield chunk.value;