perf upgrade
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { createContext, useCallback, useEffect, useState, PropsWithChildren } from 'react';
|
||||
import { createContext, useCallback, useEffect, useMemo, useState, PropsWithChildren } from 'react';
|
||||
import { SpotifyAuth } from '@/lib/spotify/auth';
|
||||
import { useAuth } from './auth-context';
|
||||
import { useConfig } from '@/hooks/use-config';
|
||||
@@ -21,6 +21,62 @@ const defaultSpotifyState: SpotifyAuthState = {
|
||||
|
||||
export const SpotifyContext = createContext<SpotifyContextType | null>(null);
|
||||
|
||||
const deepEqual = (a: unknown, b: unknown): boolean => {
|
||||
if (a === b) return true;
|
||||
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
||||
const aKeys = Object.keys(a as Record<string, unknown>);
|
||||
const bKeys = Object.keys(b as Record<string, unknown>);
|
||||
if (aKeys.length !== bKeys.length) return false;
|
||||
return aKeys.every((key) =>
|
||||
deepEqual(
|
||||
(a as Record<string, unknown>)[key],
|
||||
(b as Record<string, unknown>)[key]
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const makeSpotifyRequest = async (endpoint: string, options: RequestInit = {}) => {
|
||||
const response = await fetch(`/api/spotify/${endpoint}`, {
|
||||
...options,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = 'Request failed';
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.error || errorMessage;
|
||||
} catch {
|
||||
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (!contentType.includes('application/json')) {
|
||||
console.warn(`Non-JSON response from ${endpoint}:`, contentType);
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse JSON response from ${endpoint}:`, error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const { roles } = useAuth();
|
||||
const isAdmin = roles?.includes('Admin') || false;
|
||||
@@ -41,9 +97,9 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const [isCaptureLoading, setIsCaptureLoading] = useState(false);
|
||||
const [isResumeLoading, setIsResumeLoading] = useState(false);
|
||||
|
||||
const spotifyAuth = new SpotifyAuth(
|
||||
config.spotifyClientId,
|
||||
config.spotifyRedirectUri
|
||||
const spotifyAuth = useMemo(
|
||||
() => new SpotifyAuth(config.spotifyClientId, config.spotifyRedirectUri),
|
||||
[config.spotifyClientId, config.spotifyRedirectUri]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -126,45 +182,6 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
document.cookie = 'spotify_refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
||||
}, []);
|
||||
|
||||
const makeSpotifyRequest = async (endpoint: string, options: RequestInit = {}) => {
|
||||
const response = await fetch(`/api/spotify/${endpoint}`, {
|
||||
...options,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = 'Request failed';
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.error || errorMessage;
|
||||
} catch {
|
||||
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (!contentType.includes('application/json')) {
|
||||
console.warn(`Non-JSON response from ${endpoint}:`, contentType);
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse JSON response from ${endpoint}:`, error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const play = useCallback(async (deviceId?: string) => {
|
||||
if (!authState.isAuthenticated) return;
|
||||
|
||||
@@ -350,12 +367,17 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
try {
|
||||
const data = await makeSpotifyRequest('playback?type=state');
|
||||
const state = data.playbackState;
|
||||
|
||||
setPlaybackState(state);
|
||||
setCurrentTrack(state?.item || null);
|
||||
|
||||
|
||||
setPlaybackState((prev) => (deepEqual(prev, state ?? null) ? prev : state));
|
||||
setCurrentTrack((prev) => {
|
||||
const next = state?.item || null;
|
||||
return deepEqual(prev, next) ? prev : next;
|
||||
});
|
||||
|
||||
if (state?.device) {
|
||||
setActiveDeviceState(state.device);
|
||||
setActiveDeviceState((prev) =>
|
||||
deepEqual(prev, state.device) ? prev : state.device
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to refresh playback state:', error);
|
||||
@@ -365,8 +387,24 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
useEffect(() => {
|
||||
if (!authState.isAuthenticated) return;
|
||||
|
||||
const interval = setInterval(refreshPlaybackState, 5000);
|
||||
return () => clearInterval(interval);
|
||||
const poll = () => {
|
||||
if (document.hidden) return;
|
||||
refreshPlaybackState();
|
||||
};
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (!document.hidden) {
|
||||
refreshPlaybackState();
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(poll, 5000);
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [authState.isAuthenticated, refreshPlaybackState]);
|
||||
|
||||
const capturePlaybackState = useCallback(async () => {
|
||||
@@ -432,7 +470,7 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const contextValue: SpotifyContextType = {
|
||||
const contextValue: SpotifyContextType = useMemo(() => ({
|
||||
...authState,
|
||||
currentTrack,
|
||||
playbackState,
|
||||
@@ -461,7 +499,33 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
clearCapturedState,
|
||||
// Search
|
||||
searchTracks,
|
||||
};
|
||||
}), [
|
||||
authState,
|
||||
currentTrack,
|
||||
playbackState,
|
||||
devices,
|
||||
activeDevice,
|
||||
isLoading,
|
||||
error,
|
||||
capturedState,
|
||||
isCaptureLoading,
|
||||
isResumeLoading,
|
||||
login,
|
||||
logout,
|
||||
play,
|
||||
playTrack,
|
||||
pause,
|
||||
skipNext,
|
||||
skipPrevious,
|
||||
setVolume,
|
||||
getDevices,
|
||||
setActiveDevice,
|
||||
refreshPlaybackState,
|
||||
capturePlaybackState,
|
||||
resumePlaybackState,
|
||||
clearCapturedState,
|
||||
searchTracks,
|
||||
]);
|
||||
|
||||
if (!isAdmin) {
|
||||
return <>{children}</>;
|
||||
|
||||
Reference in New Issue
Block a user