From 602957b6c97e1fd382b4c74a79c4639137104e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Ben=C3=ADtez?= Date: Sat, 20 Jun 2026 02:13:53 -0300 Subject: [PATCH] fix(sprint-5/etapa-4b): timeout AbortController en sync de perfil al login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit upsertUserOnLogin y syncProfileFromDB adquieren el processLock de Supabase para leer/escribir la sesión. Si el servidor GoTrue está lento (token refresh), ese lock puede quedar ocupado varios segundos bloqueando loads de biblioteca/workspace. Agrega AbortController con timeout de 6s a las dos queries de auth sync en AuthContext. Si el servidor no responde a tiempo, la señal de abort cancela el fetch, postgrest-js lanza AbortError, el lock se libera, y el IIFE termina silenciosamente. El profile save ya usa fetch nativo (no le afecta el lock). Co-Authored-By: Claude Sonnet 4.6 --- src/auth/AuthContext.tsx | 23 +++++++++++++++++++---- src/auth/SupabaseAuthService.ts | 16 ++++++++-------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/auth/AuthContext.tsx b/src/auth/AuthContext.tsx index d18cc78..421aa18 100644 --- a/src/auth/AuthContext.tsx +++ b/src/auth/AuthContext.tsx @@ -77,25 +77,40 @@ export function AuthProvider({ children }: { children: ReactNode }) { // user?.id es primitivo estable: no cambia cuando se edita nombre o empresa. useEffect(() => { if (!user?.id) return + const userId = user.id + const capturedUser = user + + // 6s timeout: si el servidor no responde, la señal de abort cancela las queries + // y libera el processLock de Supabase antes de que bloquee loads de biblioteca/workspace. + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 6000) ;(async () => { try { // Upsert idempotente: crea la fila si no existe (primer login), - // ignoraDuplicates evita sobreescribir name/company editados posteriormente. - await authService.upsertUserOnLogin(user) + // ignoreDuplicates evita sobreescribir name/company editados posteriormente. + await authService.upsertUserOnLogin(capturedUser, controller.signal) // Leer company y name editado desde la tabla (pueden diferir del user_metadata de Google). - const profile = await authService.syncProfileFromDB(user.id) + const profile = await authService.syncProfileFromDB(userId, controller.signal) if (profile) { setUser((prev) => - prev?.id === user.id + prev?.id === userId ? { ...prev, company: profile.company, name: profile.name ?? prev.name } : prev ) } } catch (err) { + if (err instanceof Error && err.name === 'AbortError') return console.error('Error al sincronizar perfil:', err) + } finally { + clearTimeout(timeoutId) } })() + + return () => { + controller.abort() + clearTimeout(timeoutId) + } }, [user?.id]) // eslint-disable-line react-hooks/exhaustive-deps const signIn = useCallback(() => authService.signInWithGoogle(), []) diff --git a/src/auth/SupabaseAuthService.ts b/src/auth/SupabaseAuthService.ts index 73172d7..d88212f 100644 --- a/src/auth/SupabaseAuthService.ts +++ b/src/auth/SupabaseAuthService.ts @@ -59,12 +59,10 @@ export class SupabaseAuthService implements AuthService { // Leer company y name editados desde la tabla users (pueden diferir del user_metadata de Google). // Se llama desde AuthContext FUERA del lock de auth, en un useEffect normal. - async syncProfileFromDB(userId: string): Promise | null> { - const { data } = await supabase - .from('users') - .select('company, name') - .eq('id', userId) - .maybeSingle() + // signal permite cancelar si el servidor tarda demasiado (libera el processLock). + async syncProfileFromDB(userId: string, signal?: AbortSignal): Promise | null> { + const base = supabase.from('users').select('company, name').eq('id', userId) + const { data } = await (signal ? base.abortSignal(signal) : base).maybeSingle() if (!data) return null return { name: (data.name as string | null) ?? undefined as unknown as string, @@ -74,8 +72,9 @@ export class SupabaseAuthService implements AuthService { // Upsert idempotente al login: crea la fila si no existe, ignora si ya existe. // Se llama desde AuthContext FUERA del lock de auth, en un useEffect normal. - async upsertUserOnLogin(user: AuthUser): Promise { - const { error } = await supabase.from('users').upsert( + // signal permite cancelar si el servidor tarda demasiado (libera el processLock). + async upsertUserOnLogin(user: AuthUser, signal?: AbortSignal): Promise { + const builder = supabase.from('users').upsert( { id: user.id, name: user.name, @@ -84,6 +83,7 @@ export class SupabaseAuthService implements AuthService { }, { onConflict: 'id', ignoreDuplicates: true } ) + const { error } = await (signal ? builder.abortSignal(signal) : builder) if (error) console.error('Error al crear usuario en DB:', error) }