feat(05-05): add comprehensive error handling to JamTrack player
- Add 5 error types (file, network, download, playback, general) - Implement color-coded error display (red for critical, yellow for warnings) - Add retry button for recoverable errors (network, download, file) - Implement handleRetryError with type-specific retry logic - Add network resilience: stop polling after 3 consecutive failures - Add edge case validation: null jamClient, invalid jamTrack data - User-friendly error messages with dismiss functionality - Match Phase 3 Backing Track error handling patterns Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
90aa66bdb6
commit
eee95fe312
|
|
@ -10,6 +10,15 @@ import {
|
|||
import { setOpenJamTrack, clearOpenJamTrack } from '../../store/features/sessionUISlice';
|
||||
import { setAvailableMixdowns, setActiveMixdown } from '../../store/features/activeSessionSlice';
|
||||
|
||||
// Error types for comprehensive error handling
|
||||
const ERROR_TYPES = {
|
||||
FILE: 'file', // Red - critical
|
||||
NETWORK: 'network', // Red - critical
|
||||
DOWNLOAD: 'download', // Red - critical
|
||||
PLAYBACK: 'playback', // Yellow - warning
|
||||
GENERAL: 'general' // Yellow - warning
|
||||
};
|
||||
|
||||
const JKSessionJamTrackPlayer = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
|
|
@ -37,6 +46,7 @@ const JKSessionJamTrackPlayer = ({
|
|||
const fqIdRef = useRef(null);
|
||||
const mountedRef = useRef(true);
|
||||
const pendingSeekRef = useRef(null);
|
||||
const consecutiveFailuresRef = useRef(0);
|
||||
|
||||
// Helper: Build fqId
|
||||
const buildFqId = useCallback(async () => {
|
||||
|
|
@ -55,6 +65,18 @@ const JKSessionJamTrackPlayer = ({
|
|||
setIsLoadingSync(true);
|
||||
setError(null);
|
||||
|
||||
// Edge case validation: null jamClient
|
||||
if (!jamClient) {
|
||||
setError({ type: ERROR_TYPES.GENERAL, message: 'Native client not available. Please ensure JamKazam is running.' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Edge case validation: invalid jamTrack
|
||||
if (!jamTrack || !jamTrack.id) {
|
||||
setError({ type: ERROR_TYPES.FILE, message: 'Invalid JamTrack data' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Build fqId
|
||||
const fqId = await buildFqId();
|
||||
fqIdRef.current = fqId;
|
||||
|
|
@ -63,8 +85,7 @@ const JKSessionJamTrackPlayer = ({
|
|||
const syncResult = await dispatch(checkJamTrackSync({ jamTrack, jamClient })).unwrap();
|
||||
|
||||
if (!syncResult.isSynchronized) {
|
||||
// Download flow will be triggered by loadJamTrack
|
||||
console.log('[JamTrack] Not synchronized, will download');
|
||||
// Download flow will be triggered by loadJamTrack (no diagnostic logging)
|
||||
}
|
||||
|
||||
// Fetch available mixdowns
|
||||
|
|
@ -116,7 +137,7 @@ const JKSessionJamTrackPlayer = ({
|
|||
|
||||
} catch (err) {
|
||||
console.error('[JamTrack] Initialization error:', err);
|
||||
setError({ type: 'initialization', message: err.message || 'Failed to initialize JamTrack' });
|
||||
setError({ type: ERROR_TYPES.GENERAL, message: err.message || 'Failed to initialize JamTrack' });
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
setIsLoadingSync(false);
|
||||
|
|
@ -169,7 +190,7 @@ const JKSessionJamTrackPlayer = ({
|
|||
}
|
||||
} catch (err) {
|
||||
console.error('[JamTrack] Play error:', err);
|
||||
setError({ type: 'playback', message: 'Failed to play JamTrack' });
|
||||
setError({ type: ERROR_TYPES.PLAYBACK, message: 'Failed to play JamTrack' });
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
setIsOperating(false);
|
||||
|
|
@ -188,7 +209,7 @@ const JKSessionJamTrackPlayer = ({
|
|||
dispatch(setJamTrackState({ isPaused: true, isPlaying: false }));
|
||||
} catch (err) {
|
||||
console.error('[JamTrack] Pause error:', err);
|
||||
setError({ type: 'playback', message: 'Failed to pause JamTrack' });
|
||||
setError({ type: ERROR_TYPES.PLAYBACK, message: 'Failed to pause JamTrack' });
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
setIsOperating(false);
|
||||
|
|
@ -211,7 +232,7 @@ const JKSessionJamTrackPlayer = ({
|
|||
}));
|
||||
} catch (err) {
|
||||
console.error('[JamTrack] Stop error:', err);
|
||||
setError({ type: 'playback', message: 'Failed to stop JamTrack' });
|
||||
setError({ type: ERROR_TYPES.PLAYBACK, message: 'Failed to stop JamTrack' });
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
setIsOperating(false);
|
||||
|
|
@ -238,7 +259,7 @@ const JKSessionJamTrackPlayer = ({
|
|||
dispatch(setJamTrackState({ currentPositionMs: newPositionMs }));
|
||||
} catch (err) {
|
||||
console.error('[JamTrack] Seek error:', err);
|
||||
setError({ type: 'playback', message: 'Failed to seek' });
|
||||
setError({ type: ERROR_TYPES.PLAYBACK, message: 'Failed to seek' });
|
||||
}
|
||||
}, [isOperating, jamClient, jamTrackState.isPaused, dispatch]);
|
||||
|
||||
|
|
@ -274,7 +295,7 @@ const JKSessionJamTrackPlayer = ({
|
|||
|
||||
} catch (err) {
|
||||
console.error('[JamTrack] Mixdown change error:', err);
|
||||
setError({ type: 'playback', message: 'Failed to change mixdown' });
|
||||
setError({ type: ERROR_TYPES.PLAYBACK, message: 'Failed to change mixdown' });
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
setIsOperating(false);
|
||||
|
|
@ -291,7 +312,7 @@ const JKSessionJamTrackPlayer = ({
|
|||
dispatch(setDownloadState({ state: 'idle', progress: 0 }));
|
||||
} catch (err) {
|
||||
console.error('[JamTrack] Cancel download error:', err);
|
||||
setError({ type: 'download', message: 'Failed to cancel download' });
|
||||
setError({ type: ERROR_TYPES.DOWNLOAD, message: 'Failed to cancel download' });
|
||||
}
|
||||
}, [jamClient, dispatch]);
|
||||
|
||||
|
|
@ -316,7 +337,7 @@ const JKSessionJamTrackPlayer = ({
|
|||
|
||||
} catch (err) {
|
||||
console.error('[JamTrack] Retry download error:', err);
|
||||
setError({ type: 'download', message: 'Retry failed' });
|
||||
setError({ type: ERROR_TYPES.DOWNLOAD, message: 'Retry failed' });
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
setIsOperating(false);
|
||||
|
|
@ -324,6 +345,25 @@ const JKSessionJamTrackPlayer = ({
|
|||
}
|
||||
}, [isOperating, jamClient, jamTrack, selectedMixdownId, dispatch]);
|
||||
|
||||
// Error retry handler
|
||||
const handleRetryError = useCallback(async () => {
|
||||
if (!error || isOperating) return;
|
||||
|
||||
setError(null);
|
||||
|
||||
// Retry based on error type
|
||||
if (error.type === ERROR_TYPES.DOWNLOAD || error.type === ERROR_TYPES.FILE) {
|
||||
await handleRetryDownload();
|
||||
} else if (error.type === ERROR_TYPES.NETWORK) {
|
||||
// Re-initialize player
|
||||
if (jamTrack && jamClient) {
|
||||
const fqId = await buildFqId();
|
||||
fqIdRef.current = fqId;
|
||||
await dispatch(loadJamTrack({ jamTrack, mixdownId: selectedMixdownId, autoPlay: false, jamClient }));
|
||||
}
|
||||
}
|
||||
}, [error, isOperating, jamTrack, jamClient, selectedMixdownId, handleRetryDownload, buildFqId, dispatch]);
|
||||
|
||||
// Helper: Format milliseconds to MM:SS
|
||||
const formatTime = (ms) => {
|
||||
if (!ms || isNaN(ms)) return '00:00';
|
||||
|
|
@ -371,9 +411,19 @@ const JKSessionJamTrackPlayer = ({
|
|||
currentPositionMs: 0
|
||||
}));
|
||||
}
|
||||
|
||||
// Reset error counter on success
|
||||
consecutiveFailuresRef.current = 0;
|
||||
} catch (err) {
|
||||
console.error('[JamTrack] Polling error:', err);
|
||||
// Don't setError - polling errors are non-critical
|
||||
consecutiveFailuresRef.current += 1;
|
||||
|
||||
// After 3 consecutive failures, show error and stop polling
|
||||
if (consecutiveFailuresRef.current >= 3) {
|
||||
setError({ type: ERROR_TYPES.NETWORK, message: 'Lost connection to native client. Check if JamKazam is running.' });
|
||||
// Stop polling by setting isPlaying to false
|
||||
dispatch(setJamTrackState({ isPlaying: false }));
|
||||
}
|
||||
}
|
||||
}, pollInterval);
|
||||
|
||||
|
|
@ -396,9 +446,22 @@ const JKSessionJamTrackPlayer = ({
|
|||
return (
|
||||
<div className="jamtrack-player">
|
||||
{error && (
|
||||
<div style={{ background: error.type === 'file' || error.type === 'network' ? '#fee' : '#ffd', padding: '10px' }}>
|
||||
{error.message}
|
||||
<button onClick={() => setError(null)}>Dismiss</button>
|
||||
<div
|
||||
style={{
|
||||
background: (error.type === ERROR_TYPES.FILE || error.type === ERROR_TYPES.NETWORK || error.type === ERROR_TYPES.DOWNLOAD) ? '#fee' : '#ffd',
|
||||
padding: '10px',
|
||||
marginBottom: '10px',
|
||||
border: '1px solid',
|
||||
borderColor: (error.type === ERROR_TYPES.FILE || error.type === ERROR_TYPES.NETWORK || error.type === ERROR_TYPES.DOWNLOAD) ? '#fcc' : '#fc6'
|
||||
}}
|
||||
>
|
||||
<strong>{error.type.toUpperCase()} ERROR:</strong> {error.message}
|
||||
<div style={{ marginTop: '5px' }}>
|
||||
<button onClick={() => setError(null)}>Dismiss</button>
|
||||
{(error.type === ERROR_TYPES.NETWORK || error.type === ERROR_TYPES.DOWNLOAD || error.type === ERROR_TYPES.FILE) && (
|
||||
<button onClick={handleRetryError} style={{ marginLeft: '5px' }}>Retry</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue