From 8142b07525cea53daa4113ebd8ae9dde4025b40f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Ben=C3=ADtez?= Date: Sat, 20 Jun 2026 02:26:33 -0300 Subject: [PATCH] =?UTF-8?q?a=20ver=20si=20ahora=20s=C3=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sprints/sprint-5/ETAPA_4E_PROMPT.md | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 cowork/InQ ROI - Simulador de procesos/sprints/sprint-5/ETAPA_4E_PROMPT.md diff --git a/cowork/InQ ROI - Simulador de procesos/sprints/sprint-5/ETAPA_4E_PROMPT.md b/cowork/InQ ROI - Simulador de procesos/sprints/sprint-5/ETAPA_4E_PROMPT.md new file mode 100644 index 0000000..ff34d9d --- /dev/null +++ b/cowork/InQ ROI - Simulador de procesos/sprints/sprint-5/ETAPA_4E_PROMPT.md @@ -0,0 +1,227 @@ +# 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(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((_, 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 && ( +

{saveError}

+)} +``` + +--- + +## 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