fix(sprint-5/etapa-4g): revert ProfileSheet a raw fetch — evita processLock contention
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
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:
@@ -4,9 +4,24 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/com
|
|||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Button } from '@/components/ui/button'
|
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'
|
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 {
|
interface ProfileSheetProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
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.
|
// Actualización optimista: aplicar cambios localmente y cerrar el Sheet de inmediato.
|
||||||
// El Sheet nunca queda abierto esperando la respuesta del servidor.
|
// El Sheet nunca queda abierto esperando la respuesta del servidor.
|
||||||
// La modal de Radix bloquea todo el UI mientras está abierta, así que cerramos
|
// 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 })
|
updateUser({ name: savedName, company: savedCompany })
|
||||||
onOpenChange(false)
|
onOpenChange(false)
|
||||||
|
|
||||||
// Persistir en Supabase en background — fire & forget.
|
// Persistir en Supabase en background — fire & forget.
|
||||||
// Usamos el cliente Supabase (no fetch nativo) para que maneje el refresh de token
|
// Usamos raw fetch (no el cliente Supabase) para evitar contención con processLock.
|
||||||
// automáticamente. Como el Sheet ya cerró, processLock puede tardar lo que necesite
|
// getSession() interno de supabase.from().update() adquiere processLock y bloquea
|
||||||
// sin bloquear el UI. lockAcquireTimeout: 30s en supabase.ts garantiza que
|
// otras queries de la página mientras espera. Raw fetch lee el token de localStorage
|
||||||
// eventualmente libera si hay contención (no queda colgado para siempre).
|
// directamente — sin lock, sin overhead de auth.
|
||||||
void supabase
|
const token = getAccessToken()
|
||||||
.from('users')
|
if (!token) return
|
||||||
.update({ name: savedName, company: savedCompany ?? null })
|
|
||||||
.eq('id', user.id)
|
void fetch(
|
||||||
.then(({ error }) => {
|
`${SUPABASE_URL}/rest/v1/users?id=eq.${encodeURIComponent(user.id)}`,
|
||||||
if (error) console.error('[ProfileSheet] Error guardando perfil:', error)
|
{
|
||||||
|
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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user