From 2c3aeb62186dff4a1159b92f5fd9f78ea02a4409 Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Mon, 27 Jul 2026 23:07:24 -0700 Subject: [PATCH] fix(stems): surface archive job timeouts on mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DownloadTrackArchiveDrawer keyed hasError off jobState.state === 'failed' alone, so a stems archive that never leaves `waiting` spun forever with no error and no retry affordance. #14535 added STEMS_ARCHIVE_POLL_TIMEOUT_MS to useGetStemsArchiveJobStatus and wired the resulting isTimedOut into the web modal, but the mobile drawer was not updated — it destructured only `data`, dropping both isTimedOut and isError. That gap is not hypothetical. During the 2026-07-28 archiver outage the worker lost its Redis locks and held every concurrency slot, leaving 24 jobs parked in `waiting` for ~7 hours. `waiting` is a perfectly valid non-terminal state, so mobile users got an indeterminate spinner that could never resolve, while web users at least got an error and a retry link after 15 minutes. Fold isError and isTimedOut into hasError, matching web exactly. Co-Authored-By: Claude Opus 5 (1M context) --- .../DownloadTrackArchiveDrawer.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/mobile/src/components/download-track-archive-drawer/DownloadTrackArchiveDrawer.tsx b/packages/mobile/src/components/download-track-archive-drawer/DownloadTrackArchiveDrawer.tsx index 70c8eba9332..269caeded20 100644 --- a/packages/mobile/src/components/download-track-archive-drawer/DownloadTrackArchiveDrawer.tsx +++ b/packages/mobile/src/components/download-track-archive-drawer/DownloadTrackArchiveDrawer.tsx @@ -124,13 +124,27 @@ const DownloadTrackArchiveDrawerContent = ({ const { mutate: cancelStemsArchiveJob } = useCancelStemsArchiveJob() - const { data: jobState } = useGetStemsArchiveJobStatus({ + const { + data: jobState, + isError: isJobStatusError, + isTimedOut: isJobTimedOut + } = useGetStemsArchiveJobStatus({ jobId }) + // `isTimedOut` and `isError` have to be part of this, not just `failed`. + // A job that never leaves `waiting` — the archiver worker losing its Redis + // lock and holding every concurrency slot, for instance — is reported as a + // perfectly valid non-terminal state forever, so keying only off `failed` + // leaves the drawer spinning with no error and no retry until the user + // gives up. The shared hook already enforces STEMS_ARCHIVE_POLL_TIMEOUT_MS + // and hands back `isTimedOut`; web consumes it and mobile did not. const hasError = !isStartingDownload && - (downloadError || initiateDownloadFailed || jobState?.state === 'failed') + (downloadError || + initiateDownloadFailed || + jobState?.state === 'failed' || + (!!jobId && (isJobStatusError || isJobTimedOut))) useEffect(() => { if (hasError) {