import type { HostLibraryCache } from '../game-files.js'; export const TRANSFER_REPORT_FRESH_MS = 30_000; const nonnegative = (value: number | null | undefined): number | null => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null; /** Keep byte progress factual; only the elapsed clock advances between host reports. */ export function gameFileTransferProgress(cache: HostLibraryCache, connected: boolean, now: number) { const active = cache.status === 'downloading' || cache.status === 'verifying'; const complete = cache.status === 'ready' || cache.status === 'outdated'; const phase = active ? (cache.status as 'downloading' | 'verifying') : (cache.transfer?.phase ?? null); const total = nonnegative(cache.bytesTotal) ?? 0; const downloaded = Math.min(total, nonnegative(cache.bytesDownloaded) ?? 0); const verified = complete ? total : nonnegative(cache.bytesVerified); const phaseBytes = phase === 'verifying' ? (verified === null ? null : Math.min(total, verified)) : downloaded; const transfer = cache.transfer; const samePhase = transfer?.phase === phase; const reportedAt = Date.parse(cache.updatedAt); const reportAge = Number.isFinite(reportedAt) && Number.isFinite(now) ? Math.max(0, now - reportedAt) : null; const stale = active && !!transfer && (reportAge === null || reportAge > TRANSFER_REPORT_FRESH_MS); const live = active && connected && samePhase && !stale && reportAge !== null; const elapsed = nonnegative(transfer?.elapsedSeconds); // Stop extrapolating when reports stop. A disconnected or finished host uses its saved duration. const elapsedSeconds = elapsed === null ? null : elapsed + (active && connected && samePhase && reportAge !== null ? Math.min(reportAge, TRANSFER_REPORT_FRESH_MS) / 1000 : 0); const rate = nonnegative(transfer?.bytesPerSecond); const estimate = nonnegative(transfer?.etaSeconds); const hasProgress = phaseBytes !== null && phaseBytes > 0 && phaseBytes < total; const bytesPerSecond = live && hasProgress && rate !== null && rate > 0 ? rate : null; const etaSeconds = bytesPerSecond !== null && estimate !== null && estimate > 0 ? estimate : null; return { active, complete, phase, total, downloaded, verified, phaseBytes, elapsedSeconds, bytesPerSecond, etaSeconds, stale, live }; }