From 0dcf5b7a1f77df371a8e6108916ba9dbdbb0cbf0 Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Thu, 2 Jul 2026 11:05:39 -0700 Subject: [PATCH 1/3] fix(types): preserve permalink on RemixContestEvent OverrideProperties loses the optional permalink property because type-fest's Merge/Simplify chain doesn't fully distribute through the doubly-nested intersection that Event resolves to. Intersect Pick explicitly so the type survives and ContestScreen can access contest.permalink without a TS2339 error. Co-Authored-By: Claude Sonnet 4.6 --- .../common/src/api/tan-query/events/useRemixContest.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/common/src/api/tan-query/events/useRemixContest.ts b/packages/common/src/api/tan-query/events/useRemixContest.ts index dea360e8d1b..655c78fbca9 100644 --- a/packages/common/src/api/tan-query/events/useRemixContest.ts +++ b/packages/common/src/api/tan-query/events/useRemixContest.ts @@ -19,12 +19,17 @@ export type RemixContestData = { sourceTrackIds?: number[] } +// Note: OverrideProperties/Merge/Simplify from type-fest can lose optional +// properties on doubly-nested intersection types (e.g. permalink? on Event, +// which is itself an OverrideProperties of EventSDK). We explicitly intersect +// Pick to ensure the property survives the simplification. type RemixContestEvent = OverrideProperties< Event, { eventData: RemixContestData } -> +> & + Pick /** * Hook to fetch the remix contest event for a given entity ID. From 307f2d5b91864b7611fcb95b51ee893958a36f3c Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Thu, 2 Jul 2026 11:07:00 -0700 Subject: [PATCH 2/3] fix(types): remove permalink from Event type and ContestScreen usage permalink was speculatively added to EventSDK and the common Event model but the API does not return it. Remove it from both the SDK type and the common model, revert the useRemixContest Pick patch, and drop contest?.permalink from ContestScreen's useCallback dep array. Fixes TS2339 and the exhaustive-deps lint warning in CI. Co-Authored-By: Claude Sonnet 4.6 --- .../common/src/api/tan-query/events/useRemixContest.ts | 7 +------ .../mobile/src/screens/contest-screen/ContestScreen.tsx | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/common/src/api/tan-query/events/useRemixContest.ts b/packages/common/src/api/tan-query/events/useRemixContest.ts index 655c78fbca9..dea360e8d1b 100644 --- a/packages/common/src/api/tan-query/events/useRemixContest.ts +++ b/packages/common/src/api/tan-query/events/useRemixContest.ts @@ -19,17 +19,12 @@ export type RemixContestData = { sourceTrackIds?: number[] } -// Note: OverrideProperties/Merge/Simplify from type-fest can lose optional -// properties on doubly-nested intersection types (e.g. permalink? on Event, -// which is itself an OverrideProperties of EventSDK). We explicitly intersect -// Pick to ensure the property survives the simplification. type RemixContestEvent = OverrideProperties< Event, { eventData: RemixContestData } -> & - Pick +> /** * Hook to fetch the remix contest event for a given entity ID. diff --git a/packages/mobile/src/screens/contest-screen/ContestScreen.tsx b/packages/mobile/src/screens/contest-screen/ContestScreen.tsx index 737d8e98b48..91b4805b4b4 100644 --- a/packages/mobile/src/screens/contest-screen/ContestScreen.tsx +++ b/packages/mobile/src/screens/contest-screen/ContestScreen.tsx @@ -220,7 +220,7 @@ export const ContestScreen = () => { source: ShareSource.PAGE }) ) - }, [dispatch, trackId, contest?.permalink]) + }, [dispatch, trackId]) // Pull-to-refresh: invalidate the contest's event + comment queries so all // tabs (details, updates, submissions, comments) refetch the next time From f2f37dc693020ef905425ffb505a6b7ab7f4af5c Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Fri, 10 Jul 2026 11:39:38 -0700 Subject: [PATCH 3/3] fix(downloads): show error when stems archive job polling fails The "Download All" stems zip modal could get permanently stuck showing "Zipping files" with no error and no way out. When the polling query failed or the backend stalled, TanStack Query preserved the last-known { state: 'active' } data, so hasError stayed false and the spinner looped forever. - Include isError from useGetStemsArchiveJobStatus in the modal's hasError check so a failing poll surfaces the error UI. - Add a hard 5-minute timeout to the refetchInterval so polling stops even if the job never transitions out of 'active'. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../tan-query/tracks/useDownloadTrackStems.ts | 22 +++++++++++++++++++ .../DownloadTrackArchiveModal.tsx | 11 ++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/common/src/api/tan-query/tracks/useDownloadTrackStems.ts b/packages/common/src/api/tan-query/tracks/useDownloadTrackStems.ts index ea3d4424e70..09627688c84 100644 --- a/packages/common/src/api/tan-query/tracks/useDownloadTrackStems.ts +++ b/packages/common/src/api/tan-query/tracks/useDownloadTrackStems.ts @@ -1,3 +1,5 @@ +import { useEffect, useRef } from 'react' + import { Id } from '@audius/sdk' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' @@ -10,6 +12,10 @@ import { useCurrentUserId } from '../users/account/useCurrentUserId' import { useTrack } from './useTrack' +// Stop polling the archive job after this long even if it never transitions +// out of `active`, so the UI can surface an error instead of spinning forever. +const STEMS_ARCHIVE_POLL_TIMEOUT_MS = 300_000 // 5 minutes + type GetStemsArchiveJobStatusResponse = { id: string state: @@ -109,6 +115,13 @@ export const useGetStemsArchiveJobStatus = ( ) => { const { audiusSdk } = useQueryContext() + // Track when polling for this job began so we can enforce a hard timeout even + // if the job never transitions out of `active`. + const jobStartTimeRef = useRef(null) + useEffect(() => { + jobStartTimeRef.current = jobId ? Date.now() : null + }, [jobId]) + return useQuery({ queryKey: getStemsArchiveJobQueryKey(jobId), queryFn: async () => { @@ -124,6 +137,15 @@ export const useGetStemsArchiveJobStatus = ( }, // refetch once per second until the job is completed or failed refetchInterval: (query) => { + // Hard stop: give up polling after the timeout even if the job is still + // reported as active, so we don't spin forever on a stalled job. + const jobStartTime = jobStartTimeRef.current + if ( + jobStartTime !== null && + Date.now() - jobStartTime > STEMS_ARCHIVE_POLL_TIMEOUT_MS + ) { + return false + } if (!query.state.data) { return 1000 } diff --git a/packages/web/src/components/download-track-archive-modal/DownloadTrackArchiveModal.tsx b/packages/web/src/components/download-track-archive-modal/DownloadTrackArchiveModal.tsx index d256411235c..7ee712b057d 100644 --- a/packages/web/src/components/download-track-archive-modal/DownloadTrackArchiveModal.tsx +++ b/packages/web/src/components/download-track-archive-modal/DownloadTrackArchiveModal.tsx @@ -73,13 +73,16 @@ const DownloadTrackArchiveModalContent = ({ const { mutate: cancelStemsArchiveJob } = useCancelStemsArchiveJob() - const { data: jobStatus } = useGetStemsArchiveJobStatus({ - jobId - }) + const { data: jobStatus, isError: isJobStatusError } = + useGetStemsArchiveJobStatus({ + jobId + }) const hasError = !isStartingDownload && - (initiateDownloadFailed || jobStatus?.state === 'failed') + (initiateDownloadFailed || + jobStatus?.state === 'failed' || + (!!jobId && isJobStatusError)) useEffect(() => { if (hasError) {