From 7f1f83382d9420196b2263f2ce11f4e18f3caf61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Ben=C3=ADtez?= Date: Sat, 20 Jun 2026 20:06:51 -0300 Subject: [PATCH] =?UTF-8?q?fix(sprint-5/etapa-4g):=20revert=20ProfileSheet?= =?UTF-8?q?=20a=20raw=20fetch=20=E2=80=94=20evita=20processLock=20contenti?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit supabase.from().update() adquiere processLock internamente via getSession(), bloqueando otras queries Supabase mientras espera. Raw fetch con token de localStorage evita el lock completamente. Mantiene el patrón optimista: updateUser + onOpenChange(false) antes del fetch, que corre en background fire-and-forget con AbortSignal.timeout(10s). getAccessToken() ahora deriva el projectRef desde el hostname de SUPABASE_URL en lugar de buscar con startsWith/endsWith — más robusto y sin iteración de todas las keys. Co-Authored-By: Claude Sonnet 4.6 --- src/components/ProfileSheet.tsx | 55 ++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/src/components/ProfileSheet.tsx b/src/components/ProfileSheet.tsx index 1d14a70..f033a50 100644 --- a/src/components/ProfileSheet.tsx +++ b/src/components/ProfileSheet.tsx @@ -4,9 +4,24 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/com import { Label } from '@/components/ui/label' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' -import { supabase } from '@/lib/supabase' +import { SUPABASE_URL, SUPABASE_ANON_KEY } from '@/lib/supabase' import { useAuth } from '@/auth/AuthContext' +// Lee el access token directamente de localStorage sin pasar por el cliente Supabase. +// Evita contención con processLock — el save de perfil no necesita refresh de token +// porque es una operación rápida y el token está fresco en sesiones activas. +function getAccessToken(): string | null { + try { + const projectRef = new URL(SUPABASE_URL).hostname.split('.')[0] + const raw = localStorage.getItem(`sb-${projectRef}-auth-token`) + if (!raw) return null + const parsed = JSON.parse(raw) as { access_token?: string } + return parsed.access_token ?? null + } catch { + return null + } +} + interface ProfileSheetProps { open: boolean onOpenChange: (open: boolean) => void @@ -36,21 +51,37 @@ export function ProfileSheet({ open, onOpenChange }: ProfileSheetProps) { // Actualización optimista: aplicar cambios localmente y cerrar el Sheet de inmediato. // El Sheet nunca queda abierto esperando la respuesta del servidor. // La modal de Radix bloquea todo el UI mientras está abierta, así que cerramos - // ANTES del await para que el usuario pueda seguir trabajando sin demora. + // ANTES del fetch para que el usuario pueda seguir trabajando sin demora. updateUser({ name: savedName, company: savedCompany }) onOpenChange(false) // Persistir en Supabase en background — fire & forget. - // Usamos el cliente Supabase (no fetch nativo) para que maneje el refresh de token - // automáticamente. Como el Sheet ya cerró, processLock puede tardar lo que necesite - // sin bloquear el UI. lockAcquireTimeout: 30s en supabase.ts garantiza que - // eventualmente libera si hay contención (no queda colgado para siempre). - void supabase - .from('users') - .update({ name: savedName, company: savedCompany ?? null }) - .eq('id', user.id) - .then(({ error }) => { - if (error) console.error('[ProfileSheet] Error guardando perfil:', error) + // Usamos raw fetch (no el cliente Supabase) para evitar contención con processLock. + // getSession() interno de supabase.from().update() adquiere processLock y bloquea + // otras queries de la página mientras espera. Raw fetch lee el token de localStorage + // directamente — sin lock, sin overhead de auth. + const token = getAccessToken() + if (!token) return + + void fetch( + `${SUPABASE_URL}/rest/v1/users?id=eq.${encodeURIComponent(user.id)}`, + { + method: 'PATCH', + headers: { + apikey: SUPABASE_ANON_KEY, + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Prefer: 'return=minimal', + }, + body: JSON.stringify({ name: savedName, company: savedCompany ?? null }), + signal: AbortSignal.timeout(10000), + } + ) + .then((res) => { + if (!res.ok) console.error('[ProfileSheet] Error guardando perfil:', res.status) + }) + .catch((err) => { + console.error('[ProfileSheet] Error guardando perfil:', err) }) }