fix(sprint-5/etapa-4g): revert ProfileSheet a raw fetch — evita processLock contention
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 20:06:51 -03:00
parent 1a4650d186
commit 7f1f83382d

View File

@@ -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)
})
}