Files
inq-roi-simulador-web/sprints/sprint-5/ETAPA_4E_PROMPT.md
Marcos Benítez 882edd230d
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
a ver si ahora sí guarda el perfil
2026-06-20 19:06:58 -03:00

228 lines
8.5 KiB
Markdown

# Sprint 5 — Etapa 4E: Fix definitivo del freeze al guardar perfil
## Contexto
InQ ROI — React 18 + Vite + TypeScript + TanStack Router + Zustand + shadcn/ui + Supabase v2.106.2.
Etapas 4B, 4C, 4D intentaron corregir el freeze al guardar perfil. Ninguna resolvió el problema. Esta etapa identifica y corrige la causa raíz real.
---
## Causa raíz real — auditada en código fuente de `@supabase/auth-js`
### Componente 1: `getSessionSafe()` abandona Promises en `PROCESS_LOCKS`
`getSessionSafe()` en `src/lib/supabase.ts` está implementada así:
```ts
await Promise.race([
supabase.auth.getSession(), // Promise A — va al chain de processLock
new Promise((resolve) => setTimeout(resolve, 4000)), // timeout 4s
])
```
Cuando el timeout gana, `getSessionSafe()` resuelve. **Pero la Promise A de `getSession()` sigue corriendo en background.** Internamente, `getSession()` llamó `processLock(name, acquireTimeout, fn_session)`, que actualizó `PROCESS_LOCKS[name] = op_A` **antes de ceder el hilo**. La operación `op_A` está en vuelo (token refresh via red) con `PROCESS_LOCKS[name]` apuntando a ella.
### Componente 2: `lockAcquireTimeout = 5000ms` demasiado corto
El cliente Supabase usa `lockAcquireTimeout = 5000ms` por defecto. Cada llamada a `getSession()` pasa ese timeout a `processLock(name, 5000, fn)`.
### La cadena del freeze
```
t=0 → library-store.load() llama getSessionSafe()
→ getSession() → processLock → PROCESS_LOCKS[name] = op_A
→ op_A empieza: token refresh (request HTTP a Supabase Auth)
t=4s → getSessionSafe() timeout gana → resuelve
→ op_A sigue corriendo en background (PROCESS_LOCKS[name] = op_A, PENDIENTE)
t=4s → library queries lanzan: supabaseGroupRepo.getAll() etc.
→ _getAccessToken() → getSession() → _acquireLock(5000, fn_B)
→ processLock(5000): previousOperation = op_A (aún pendiente)
→ espera hasta que op_A resuelva... o hasta 5s
t=9s → ProcessLockAcquireTimeoutError para op_B
→ PROCESS_LOCKS[name] = wrapper_B
→ wrapper_B hace: await previousOperation (= op_A, AÚN COLGADA)
→ wrapper_B nunca resuelve mientras op_A esté colgada
t=X → Usuario abre ProfileSheet → hace click en Guardar
→ supabase.from('users').update() → getSession() → processLock
→ previousOperation = wrapper_B (NUNCA RESUELVE)
→ FREEZE PERMANENTE
```
La 4D fue 100% correcta: el callback de `onAuthStateChange` ya no tiene queries. Pero el freeze viene de esta cadena completamente diferente, que ocurre en `getSessionSafe()` de los stores.
---
## Fix — 4 archivos
### Cambio 1: `src/lib/supabase.ts`
Dos cambios:
**A) Aumentar `lockAcquireTimeout` a 30s:**
```ts
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
lock: processLock,
// Aumentado de 5000 (default) a 30000ms.
// Con 5s, un token refresh lento dispara ProcessLockAcquireTimeoutError,
// cuyo handler hace await previousOperation (colgada) → chain bloqueado permanente.
// 30s da margen para refresh en redes lentas o cold starts de Supabase Auth.
lockAcquireTimeout: 30000,
},
})
```
**B) Eliminar `getSessionSafe()`:**
Eliminar TODA la función `getSessionSafe()` — declaración y export. No dejar ni el tipo, ni un stub.
La función tenía un problema de diseño: el `Promise.race([getSession(), timeout])` abandona la Promise de `getSession()` cuando gana el timeout. La Promise abandonada deja una operación en `PROCESS_LOCKS[name]` que bloquea el chain.
### Cambio 2: `src/store/library-store.ts`
Eliminar la línea `await getSessionSafe()` y el import correspondiente:
```ts
// ELIMINAR este import:
import { getSessionSafe } from '@/lib/supabase'
// ELIMINAR esta línea en load():
await getSessionSafe()
```
No reemplazar con nada. Las queries a Supabase ya llaman `getSession()` internamente vía `_getAccessToken()`, que espera `initializePromise` automáticamente. El `Promise.race` de 15s que ya existe es suficiente como safety net general.
### Cambio 3: `src/store/process-store.ts`
Mismo tratamiento — hay 2 usos de `getSessionSafe()` (líneas 41 y 57):
```ts
// ELIMINAR este import:
import { getSessionSafe } from '@/lib/supabase'
// ELIMINAR ambas líneas:
await getSessionSafe() // una en loadProcess(), otra en loadActivities()
```
No reemplazar. Misma razón que en library-store.
### Cambio 4: `src/features/report/useReportData.ts`
Mismo tratamiento — 1 uso:
```ts
// ELIMINAR este import:
import { getSessionSafe } from '@/lib/supabase'
// ELIMINAR la línea:
await getSessionSafe()
```
### Cambio 5: `src/components/ProfileSheet.tsx`
Agregar timeout de 10s + estado de error visual. La red de seguridad: si en algún caso extremo la operación tarde más de 10s, el botón se re-habilita y se muestra un mensaje, en lugar de quedar congelado indefinidamente.
```tsx
const [saveError, setSaveError] = useState<string | null>(null)
useEffect(() => {
if (open && user) {
setName(user.name)
setCompany(user.company ?? '')
setSaveError(null) // ← limpiar error al abrir
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open])
async function handleSave() {
if (!user || !name.trim()) return
setSaving(true)
setSaveError(null)
try {
const savePromise = supabase
.from('users')
.update({ name: name.trim(), company: company.trim() || null })
.eq('id', user.id)
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(
() => reject(new Error('La operación tardó demasiado. Revisá tu conexión e intentá de nuevo.')),
10_000
)
)
const { error } = await Promise.race([savePromise, timeoutPromise])
if (error) throw error
updateUser({ name: name.trim(), company: company.trim() || undefined })
onOpenChange(false)
} catch (err) {
setSaveError(err instanceof Error ? err.message : 'Error al guardar')
} finally {
setSaving(false)
}
}
```
En el JSX del Sheet, mostrar el error si existe (después del bloque de inputs, antes del footer):
```tsx
{saveError && (
<p className="text-xs text-destructive mt-2">{saveError}</p>
)}
```
---
## Verificación — qué buscar en el código fuente de Supabase
Si querés confirmar antes de implementar, estos son los puntos exactos a verificar en `node_modules/@supabase/auth-js/dist/module/`:
- `locks.js`: función `processLock` — el `catch (e instanceof LockAcquireTimeoutError)` hace `await previousOperation` → si `previousOperation` nunca resuelve, el chain queda bloqueado
- `GoTrueClient.js` línea ~25: `lockAcquireTimeout: 5000` — el default que se pisa con el cambio en `createClient()`
- `GoTrueClient.js` línea ~2239: `getSession()` usa `this.lockAcquireTimeout` (no -1)
---
## Qué NO entra
- Cambios en `AuthContext.tsx` o `SupabaseAuthService.ts` (la 4D fue correcta)
- Cambios en el sistema de simulación, heatmap, o ComparisonTable
- Ninguna feature nueva
- No agregar nuevos `getSessionSafe()` equivalentes en ningún lado
---
## Orden de ejecución
1. `src/lib/supabase.ts`: agregar `lockAcquireTimeout: 30000`, eliminar `getSessionSafe()`
2. `src/store/library-store.ts`: eliminar import + `await getSessionSafe()`
3. `src/store/process-store.ts`: eliminar import + ambos `await getSessionSafe()`
4. `src/features/report/useReportData.ts`: eliminar import + `await getSessionSafe()`
5. `src/components/ProfileSheet.tsx`: agregar `saveError` state + timeout 10s + UI
6. `npm run build` — debe dar 0 errores TypeScript
7. `npm run test` — 528+ Vitest verdes
8. `git push origin main` — OBLIGATORIO antes de validar en producción
9. Commit: `fix(sprint-5/etapa-4e): lockAcquireTimeout 30s + eliminar getSessionSafe — corta freeze en guardar perfil`
---
## Criterios de validación
- [ ] Guardar nombre en ProfileSheet → app NO se congela → nombre actualizado en dropdown
- [ ] Guardar empresa → app NO se congela → empresa aparece en dropdown
- [ ] Cmd+R después de guardar → valores persisten (Supabase)
- [ ] Biblioteca carga correctamente después de eliminar `getSessionSafe()` de library-store
- [ ] Workspace carga correctamente después de eliminar `getSessionSafe()` de process-store
- [ ] Reporte carga correctamente después de eliminar `getSessionSafe()` de useReportData
- [ ] Si save tarda > 10s → botón se re-habilita → mensaje de error visible (no freeze)
- [ ] `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