perf upgrade

This commit is contained in:
2026-07-12 21:26:16 -07:00
parent ec334bbed4
commit 778f0f7994
74 changed files with 1522 additions and 1014 deletions
+116 -52
View File
@@ -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}</>;