diff --git a/src/App.tsx b/src/App.tsx index f1020f4..15a34ec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -37,9 +37,10 @@ export default function App() { if (session?.user) fetchProfile() }) - const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { + const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => { setUser(session?.user ?? null) - if (session?.user) fetchProfile() + // Only fetch profile on actual sign-in, not token refresh + if (event === 'SIGNED_IN' && session?.user) fetchProfile() }) return () => subscription.unsubscribe() diff --git a/src/hooks/useListeningProgress.ts b/src/hooks/useListeningProgress.ts index beb8d22..2ffda8f 100644 --- a/src/hooks/useListeningProgress.ts +++ b/src/hooks/useListeningProgress.ts @@ -6,29 +6,33 @@ type ProgressMap = Map let cachedProgress: ProgressMap = new Map() let lastFetch = 0 +let progressInFlight: Promise | null = null export function useListeningProgress() { - const user = useAuthStore((s) => s.user) + const userId = useAuthStore((s) => s.user?.id) const [progress, setProgress] = useState(cachedProgress) useEffect(() => { - if (!user) { + if (!userId) { cachedProgress = new Map() setProgress(cachedProgress) return } - // Refetch at most every 10s + // Refetch at most every 30s (was 10s — too aggressive) const now = Date.now() - if (now - lastFetch < 10_000 && cachedProgress.size > 0) { + if (now - lastFetch < 30_000 && cachedProgress.size > 0) { setProgress(cachedProgress) return } - supabase + // Deduplicate concurrent calls + if (progressInFlight) return + + progressInFlight = supabase .from('listen_history') .select('podcast_id, progress_seconds, podcast:podcasts(duration_seconds)') - .eq('user_id', user.id) + .eq('user_id', userId) .eq('completed', false) .gt('progress_seconds', 5) .then(({ data }) => { @@ -46,7 +50,8 @@ export function useListeningProgress() { lastFetch = Date.now() setProgress(map) }) - }, [user]) + .finally(() => { progressInFlight = null }) + }, [userId]) return progress } diff --git a/src/hooks/useNotificationPolling.ts b/src/hooks/useNotificationPolling.ts index a35fa8e..8ed069a 100644 --- a/src/hooks/useNotificationPolling.ts +++ b/src/hooks/useNotificationPolling.ts @@ -12,7 +12,7 @@ export function useNotificationPolling() { fetch(user.id) - intervalRef.current = setInterval(() => fetch(user.id), 30000) + intervalRef.current = setInterval(() => fetch(user.id), 60000) function handleVisibility() { if (document.visibilityState === 'visible' && user) fetch(user.id) diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 00f7551..f467c46 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -73,15 +73,14 @@ export function Home() { setInProgress(items) } - // Fetch real stats for hero + // Compute stats for hero (non-logged-in) — 2 queries instead of 3 if (!user) { - const [{ count: podcastCount }, { data: playsData }, { count: creatorCount }] = await Promise.all([ - supabase.from('podcasts').select('*', { count: 'exact', head: true }), + const [{ data: allPlays }, { count: creatorCount }] = await Promise.all([ supabase.from('podcasts').select('plays_count'), supabase.from('profiles').select('*', { count: 'exact', head: true }), ]) - const totalPlays = playsData?.reduce((sum: number, p: any) => sum + (p.plays_count || 0), 0) || 0 - setStats({ plays: totalPlays, creators: creatorCount || 0, podcasts: podcastCount || 0 }) + const totalPlays = allPlays?.reduce((sum: number, p: any) => sum + (p.plays_count || 0), 0) || 0 + setStats({ plays: totalPlays, creators: creatorCount || 0, podcasts: allPlays?.length || 0 }) } setLoading(false) diff --git a/src/stores/auth.ts b/src/stores/auth.ts index a4be5ca..c032862 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -13,6 +13,9 @@ interface AuthState { signOut: () => Promise } +// Deduplicate fetchProfile — prevents double-fetch on init (getSession + onAuthStateChange) +let profileInFlight: Promise | null = null + export const useAuthStore = create((set, get) => ({ user: null, profile: null, @@ -24,31 +27,40 @@ export const useAuthStore = create((set, get) => ({ fetchProfile: async () => { const { user } = get() - if (!user) return set({ profile: null }) + if (!user) { set({ profile: null }); return } - const { data, error } = await supabase - .from('profiles') - .select('*') - .eq('id', user.id) - .maybeSingle() + // If already fetching, reuse the same promise + if (profileInFlight) return profileInFlight - if (error) { - console.error('Error fetching profile:', error) - return set({ profile: null }) - } - - // Auto-create profile if it doesn't exist (trigger may have failed) - if (!data) { - const username = user.user_metadata?.username || user.email?.split('@')[0] || 'user' - const { data: newProfile } = await supabase + profileInFlight = (async () => { + const { data, error } = await supabase .from('profiles') - .insert({ id: user.id, username }) - .select() - .single() - return set({ profile: newProfile }) - } + .select('*') + .eq('id', user.id) + .maybeSingle() - set({ profile: data }) + if (error) { + console.error('Error fetching profile:', error) + set({ profile: null }) + return + } + + // Auto-create profile if it doesn't exist (trigger may have failed) + if (!data) { + const username = user.user_metadata?.username || user.email?.split('@')[0] || 'user' + const { data: newProfile } = await supabase + .from('profiles') + .insert({ id: user.id, username }) + .select() + .single() + set({ profile: newProfile }) + return + } + + set({ profile: data }) + })().finally(() => { profileInFlight = null }) + + return profileInFlight }, signOut: async () => { diff --git a/src/stores/notifications.ts b/src/stores/notifications.ts index 667e93d..56bdeae 100644 --- a/src/stores/notifications.ts +++ b/src/stores/notifications.ts @@ -10,23 +10,36 @@ interface NotificationsStore { markAllRead: (userId: string) => Promise } +// Deduplicate and throttle notification fetches (min 15s between fetches) +let notifInFlight: Promise | null = null +let notifLastFetch = 0 + export const useNotificationsStore = create((set) => ({ notifications: [], unreadCount: 0, fetch: async (userId) => { - const { data } = await supabase - .from('notifications') - .select('*') - .eq('user_id', userId) - .order('created_at', { ascending: false }) - .limit(50) - if (data) { - set({ - notifications: data, - unreadCount: data.filter((n) => !n.read).length, - }) - } + const now = Date.now() + if (now - notifLastFetch < 15_000) return + if (notifInFlight) return notifInFlight + + notifInFlight = (async () => { + const { data } = await supabase + .from('notifications') + .select('*') + .eq('user_id', userId) + .order('created_at', { ascending: false }) + .limit(50) + if (data) { + set({ + notifications: data, + unreadCount: data.filter((n) => !n.read).length, + }) + } + notifLastFetch = Date.now() + })().finally(() => { notifInFlight = null }) + + return notifInFlight }, markRead: async (id) => {