# Sprint 5 — Etapa 4D: Fix definitivo — deadlock en onAuthStateChange

## Causa raíz real (auditada en código fuente de supabase-js v2.106.2)

La línea 3213 de `GoTrueClient.js` documenta explícitamente:

> **"A callback can be an `async` function and it runs synchronously during the processing of the changes causing the event. You can easily create a dead-lock by using `await` on a call to another method of the Supabase library."**

`SupabaseAuthService.onAuthStateChange` viola exactamente este contrato. El callback hace `await supabase.from('users').select('company, name')` **desde dentro del callback de auth**, que corre con el lock interno de GoTrueClient activo (`lockAcquired = true`).

La cadena de deadlock:

```
INITIAL_SESSION fires
  → GoTrueClient adquiere lock (lockAcquired = true)
  → fn() (operación auth) corre → llama _notifyAllSubscribers
  → _notifyAllSubscribers await nuestro callback (via Promise.all)
  → nuestro callback llama supabase.from('users').select(...)
  → select() necesita token → llama getSession() → llama _acquireLock()
  → _acquireLock ve lockAcquired = true → entra al path REENTRANT (sin timeout)
  → path reentrant espera a fn() → fn() espera al callback → callback espera al select → DEADLOCK CIRCULAR
  → lockAcquired queda true PARA SIEMPRE
  → toda llamada posterior a getSession() cuelga indefinidamente
  → incluye supabase.from('users').update() del perfil → app freezes
```

Los tres fixes anteriores (guard en onAuthStateChange, useEffect [open], user?.id en LibraryPage) no resolvieron el problema porque ninguno tocó la causa real: queries de Supabase dentro del callback de auth.

---

## Fix — arquitectura correcta

El callback de `onAuthStateChange` debe ser **sincrónico en su lógica de auth**: recibir el evento, construir el AuthUser básico, llamar `callback()`. Los side effects que necesitan DB (upsert, lectura de company/name) se ejecutan **fuera del lock**, en un `useEffect` de React.

### Cambio 1 — `src/auth/SupabaseAuthService.ts`

Eliminar TODAS las llamadas a `supabase.from(...)` del interior del callback. El callback solo llama `callback(buildAuthUser(session.user))`:

```ts
onAuthStateChange(callback: (user: AuthUser | null) => void): () => void {
  const { data: { subscription } } = supabase.auth.onAuthStateChange(
    (event, session) => {  // ← NO async: no necesita await
      try {
        if (!session?.user) {
          if (event !== 'INITIAL_SESSION') callback(null)
          return
        }
        if (event !== 'INITIAL_SESSION' && event !== 'SIGNED_IN') return

        // Llamar callback INMEDIATAMENTE con datos básicos del token OAuth.
        // NO hacer queries a Supabase desde aquí — el lock de auth está activo
        // y cualquier llamada reentrante causaría deadlock permanente (ver GoTrueClient.js L3213).
        // La sincronización con la tabla users (company, name, upsert) ocurre en AuthContext.
        callback(this.buildAuthUser(session.user))
      } catch (err) {
        console.error('Error en onAuthStateChange:', err)
        if (session?.user) callback(this.buildAuthUser(session.user))
        else if (event !== 'INITIAL_SESSION') callback(null)
      }
    }
  )
  return () => subscription.unsubscribe()
}
```

Agregar también un método público en `SupabaseAuthService` para la sincronización con DB (usado desde AuthContext):

```ts
async syncProfileFromDB(userId: string): Promise<Pick<AuthUser, 'name' | 'company'> | null> {
  const { data } = await supabase
    .from('users')
    .select('company, name')
    .eq('id', userId)
    .maybeSingle()
  if (!data) return null
  return {
    name: (data.name as string | null) ?? undefined as unknown as string,
    company: (data.company as string | null) ?? undefined,
  }
}

async upsertUserOnLogin(user: AuthUser): Promise<void> {
  const { error } = await supabase.from('users').upsert(
    {
      id: user.id,
      name: user.name,
      avatar_url: user.avatarUrl ?? null,
      platform_role: user.platformRole,
    },
    { onConflict: 'id', ignoreDuplicates: true }
  )
  if (error) console.error('Error al crear usuario en DB:', error)
}
```

### Cambio 2 — `src/auth/AuthContext.tsx`

Agregar un `useEffect([user?.id])` que sincroniza con la tabla `users` DESPUÉS de que el auth se estableció — fuera del lock. También necesita `import { supabase } from '@/lib/supabase'`.

```tsx
// Después del useEffect existente que suscribe a onAuthStateChange:

// Sincronización con tabla users FUERA del lock de auth.
// Se ejecuta al cambiar de usuario (login/logout), NO en cada updateUser().
// user?.id es estable: no cambia cuando se edita nombre o empresa.
useEffect(() => {
  if (!user?.id) return

  ;(async () => {
    try {
      // Upsert idempotente: crea la fila si no existe (primer login),
      // ignora si ya existe (no sobreescribe ediciones posteriores).
      await authService.upsertUserOnLogin(user)

      // Leer company y name editado desde la tabla (pueden diferir del user_metadata de Google).
      const profile = await authService.syncProfileFromDB(user.id)
      if (profile) {
        setUser((prev) =>
          prev?.id === user.id
            ? {
                ...prev,
                company: profile.company,
                name: profile.name ?? prev.name,
              }
            : prev
        )
      }
    } catch (err) {
      console.error('Error al sincronizar perfil:', err)
    }
  })()
}, [user?.id])  // eslint-disable-line react-hooks/exhaustive-deps
```

**Comportamientos garantizados:**
- Login inicial → callback establece usuario básico → useEffect sincroniza con DB ✓
- Page refresh → idem ✓
- Profile save → `updateUser()` crea nueva referencia pero `user?.id` no cambia → useEffect NO dispara → datos locales no sobreescritos ✓
- Token refresh (TOKEN_REFRESHED) → callback retorna inmediatamente → useEffect no dispara ✓
- Logout → `user?.id` = null → useEffect hace early return ✓
- Re-login → `user?.id` cambia → useEffect re-sincroniza ✓

---

## Cambios en `src/auth/types.ts`

Agregar `syncProfileFromDB` y `upsertUserOnLogin` a la interfaz `AuthService` si existe, o dejar como métodos concretos en `SupabaseAuthService` sin agregar a la interfaz (preferible si la interfaz es abstracta).

Verificar que `AuthService` interface no requiera los métodos nuevos — son implementación concreta de Supabase.

---

## Qué NO entra en esta etapa

- Cambios en ProfileSheet, LibraryPage, router.tsx
- Cambios en el sistema de simulación o heatmap
- Ninguna feature nueva

---

## Orden de ejecución

1. Modificar `SupabaseAuthService.ts`:
   - Convertir callback de `onAuthStateChange` a no-async
   - Eliminar upsert y select del interior del callback
   - Agregar métodos `syncProfileFromDB()` y `upsertUserOnLogin()`
2. Modificar `AuthContext.tsx`:
   - Agregar import de `supabase` si no existe
   - Agregar `useEffect([user?.id])` con la lógica de sincronización
3. `npm run build` — 0 errores TypeScript
4. `npm run test` — 528+ Vitest verdes
5. **`git push origin main`** — obligatorio antes de validar en producción
6. Commit: `fix(sprint-5/etapa-4d): mover queries DB fuera de onAuthStateChange — corta deadlock lockAcquired permanente`

---

## Criterios de validación

- [ ] Guardar nombre en el Sheet → app NO se congela → nombre actualizado en el dropdown
- [ ] Guardar empresa → app NO se congela → empresa aparece en el dropdown
- [ ] Cmd+R después de guardar → los valores persisten (están en Supabase)
- [ ] Al cargar la app (page refresh) → company y name se leen correctamente desde la DB
- [ ] Primer login (cuenta nueva) → usuario creado en tabla users ✓
- [ ] Token refresh en background → no causa ningún re-render ni error
- [ ] No hay `console.error` de deadlock en la consola del browser
- [ ] `npm run build` limpio
- [ ] `npm run test` — 528+ Vitest verdes
- [ ] **`git push origin main` ejecutado** antes de validar en producción
- [ ] **Validación visual del director** antes del OK formal
