fix(sprint-5/etapa-4b): timeout AbortController en sync de perfil al login
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 02:13:53 -03:00
parent 3d3ae99125
commit 602957b6c9
2 changed files with 27 additions and 12 deletions

View File

@@ -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(), [])

View File

@@ -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<Pick<AuthUser, 'name' | 'company'> | 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<Pick<AuthUser, 'name' | 'company'> | 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<void> {
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<void> {
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)
}