Refactorizar MailVault: del monolito a los módulos
MailVault ha crecido rápido. En las últimas semanas hemos publicado atajos de teclado, bandeja unificada, plantillas de correo, información de remitentes, reglas de limpieza automática, deshacer envío, mover a carpeta y una capa de transporte completa sobre la Microsoft Graph API. Las funciones iban llegando, la gente estaba contenta — y el código empezaba a crujir bajo el peso.
El almacén central de estado — un único archivo de Zustand llamado mailStore.js — había superado las 4300 líneas. Se ocupaba de todo: gestión de cuentas, carga de correos, caché, búsqueda, conversaciones, la rama de la API de Graph, sincronización delta con CONDSTORE, operaciones en bloque y más. El main.rs del backend en Rust iba por el mismo camino. Componentes como SettingsPage y EmailViewer se habían hinchado hasta convertirse en muros monolíticos de JSX. Todo seguía funcionando, pero añadir cualquier cosa nueva significaba abrirse paso por un laberinto.
Tocaba refactorizar — no porque algo estuviera roto, sino porque la velocidad de sacar funciones nuevas empezaba a caer. Esta entrada cuenta qué hicimos, por qué y hacia dónde vamos.
El problema de tener un único store gigante
Zustand va muy bien para gestionar el estado. Pero cuando un único archivo de almacén pasa de unos pocos miles de líneas, empiezan los problemas prácticos: todas las funciones tocan el mismo archivo, los conflictos de fusión se vuelven rutina y cuesta razonar sobre qué depende de qué. En nuestro mailStore, la lógica de carga de correos estaba entrelazada con el código de transporte de la API de Graph, la gestión de la caché LRU convivía con el estado de la búsqueda y los algoritmos de conversaciones se mezclaban con el cambio de cuenta.
En Rust se repetía el mismo patrón. main.rs contenía los helpers de rutas de Maildir, las operaciones con el llavero, la gestión de la caché, el tratamiento de adjuntos, la lógica de notificaciones y utilidades del sistema — todo en un solo archivo. En el módulo de IMAP, el pool de conexiones estaba enredado con las operaciones a nivel de protocolo.
Carga de correo en dos flujos
El mayor cambio arquitectónico de la v2.2.0 fue rediseñar cómo se cargan los correos al cambiar de cuenta o de carpeta. El enfoque antiguo tenía seis fases solapadas que se ejecutaban en una secuencia frágil: mirar la caché, mostrar los datos cacheados, conectar por IMAP, descargar cabeceras, fusionar resultados, actualizar la interfaz. Si cambiabas de cuenta rápido, esas fases podían entrelazarse y acababas viendo un instante correos de la cuenta equivocada: el fallo de los "correos vacíos".
El diseño nuevo es un modelo limpio de dos flujos. Cuando activas una cuenta, arrancan en paralelo dos flujos independientes:
El flujo 1 (local) carga desde disco las cabeceras en caché y los correos guardados en local. Es casi instantáneo — la interfaz se llena en milisegundos gracias a la caché headers.json y a un nuevo local-index.json que evita tener que analizar el MIME de cada archivo .eml del disco.
El flujo 2 (servidor) se conecta por IMAP (o a la API de Graph en las cuentas de Microsoft), consulta el estado actual del servidor y fusiona los cambios en la vista local. Aparecen los correos nuevos, desaparecen los borrados, se actualizan las marcas de leído y no leído — todo sin parpadeos y sin sustituir lo que ya está en pantalla.
Los dos flujos van envueltos en un AbortController. Si cambias de cuenta antes de que termine cualquiera de ellos, el trabajo en curso se cancela limpiamente. Sin condiciones de carrera, sin datos obsoletos, sin "correos vacíos".
Extracción de módulos
La primera fase de la refactorización se centró en sacar el código que no tenía por qué vivir dentro del almacén ni dentro de main.rs.
En la parte de JavaScript extrajimos:
Configuración de Graph — la correspondencia de nombres de carpeta de Microsoft Graph (Inbox → INBOX, Sent Items → Sent, etc.) estaba duplicada tres veces en el código. Ahora vive en un único módulo, graphConfig.js.
Cachés LRU — la caché de cuentas (el estado de correo de hasta 8 cuentas, para cambiar entre ellas al instante) y la caché de cuerpos (el contenido de los correos, con límites de memoria configurables) se extrajeron a un módulo de caché propio, con una implementación LRU compartida.
Búsqueda — el estado y la lógica de búsqueda estaban enredados en el almacén principal pese a ser en buena medida independientes. Sacarlos de ahí hizo más fácil de seguir tanto el almacén como el código de búsqueda.
Trazas de rendimiento — teníamos llamadas sueltas a console.time repartidas por el código de carga. Las sustituimos por una utilidad estructurada, perfTrace, que se puede activar y desactivar, lo que deja mucho más limpia la depuración del rendimiento.
Descomposición de componentes
Dos componentes de React hacían demasiadas cosas: SettingsPage y EmailViewer.
SettingsPage renderizaba todas las pestañas de ajustes en un solo archivo — cuentas, apariencia, atajos de teclado, notificaciones, plantillas, limpieza automática y más. Lo dividimos en componentes de pestaña independientes, cada uno en un subdirectorio settings/. El componente padre ahora solo se ocupa de cambiar de pestaña y de renderizar la activa.
EmailViewer había crecido hasta encargarse de mostrar un correo suelto, la vista de conversación, el renderizado de adjuntos, los distintivos de verificación DKIM, la información del remitente y las acciones de responder y reenviar — todo en un mismo componente. Extrajimos ThreadView, EmailHeader y AttachmentBar como componentes con un cometido claro, en un subdirectorio email/.
El resultado son archivos más pequeños, más fáciles de recorrer, de probar y de modificar sin romper por accidente algo que no tiene que ver.
El plan de cinco fases
Lo que salió en la v2.2.0 fue el principio, no el final. Diseñamos un plan de refactorización en cinco fases que vamos ejecutando poco a poco:
Fase 1: bases compartidas — extraer las constantes y utilidades comunes (configuración de Graph, constantes de Rust, trazas de rendimiento). Hecho.
Fase 2: backend en Rust — dividir main.rs en módulos con un cometido claro: operaciones de Maildir, acceso al llavero, gestión de la caché, tratamiento de adjuntos, notificaciones y utilidades del sistema. El módulo de IMAP se parte en operaciones a nivel de protocolo y gestión del pool de conexiones.
Fase 3: descomposición del almacén — crear almacenes propios useSearchStore y useThreadStore, y extraer el transporte de la API de Graph, la gestión de la caché y la carga de correos a módulos de servicio. El objetivo: que mailStore baje de 4300 líneas a unas 2000.
Fase 4: extracción de componentes — seguir troceando los componentes grandes. AccountModal se divide en subcomponentes de detección de servidor y configuración de OAuth. Los demás componentes sobredimensionados reciben el mismo trato.
Fase 5: limpieza — eliminar código muerto, unificar utilidades duplicadas (como hasValidCredentials, que existía en varios sitios) y dividir el módulo de utilidades de base de datos en archivos con un cometido claro.
Refactorizar sin regresiones
La parte más difícil de cualquier refactorización es asegurarse de que nada se rompe. MailVault maneja el correo de la gente: si el cambio de cuenta empieza a mostrar los mensajes equivocados o reaparece una condición de carrera en la carga, eso es un problema de verdad.
Nuestro planteamiento: refactorizar un límite cada vez, comprobar que todo sigue funcionando y pasar al siguiente. Los más de 600 tests automatizados repartidos en cuatro capas nos dan una red de seguridad, pero además probamos a mano después de cada fase: cambiar de cuenta muy deprisa, quedarnos sin conexión a mitad de carga, probar con el buzón de 17.000 correos y comprobar que tanto las cuentas de la API de Graph como las de IMAP normal se comportan bien.
También tomamos una decisión deliberada sobre la arquitectura del almacén. En vez de dividirlo todo en muchos almacenes diminutos (lo que puede crear sus propios problemas de coordinación), optamos por un enfoque híbrido: almacenes independientes para lo que de verdad es independiente (búsqueda y conversaciones), y el estado de cuentas y correos junto en mailStore, porque están muy acoplados. El objetivo es la claridad, no la pureza.
Qué gana quien lo usa
Una refactorización es invisible por diseño: la app debería verse y sentirse exactamente igual. Pero los efectos son reales:
Cambio de cuenta más rápido — con la arquitectura de dos flujos ves tus correos casi al instante al cambiar de cuenta, mientras la sincronización con el servidor ocurre en silencio en segundo plano.
Se acabaron los fallos de carga — el error de los "correos vacíos" que aparecía al cambiar de cuenta muy deprisa ha desaparecido, junto con otras condiciones de carrera del antiguo código de carga secuencial.
Desarrollo más rápido de nuevas funciones — esta es la grande. Un código más limpio significa que las funciones nuevas se construyen y se publican antes. La próxima función de "descargarlo todo", las copias de seguridad automáticas programadas y otras mejoras muy pedidas llegarán antes porque el código es más fácil de manejar.
Estamos construyendo MailVault para que dure. El correo es una tecnología con décadas encima a la que las herramientas modernas no han hecho mucho caso, y las soluciones de copia que existen son demasiado técnicas o demasiado torpes. Hacer sostenible el código es como nos aseguramos de que la app siga mejorando, no solo este mes, sino a largo plazo.