Filarr para móvil, semana en curso: una sincronización que dice la verdad

Esta semana en la app móvil de Filarr: cinco arreglos de sincronización, dos estados de error permanentes, tu almacenamiento real y 3326 pruebas en verde.

Mathis Belouar-Pruvot

· fundador de Filarr · · 9 min de lectura

Esta semana en la app móvil: la sincronización dice la verdad. Cinco arreglos, dos nuevos estados de error permanentes, una pantalla de inicio que por fin muestra tu uso de almacenamiento y 3326 pruebas en verde. La app móvil de Filarr sale en uno o dos meses, aproximadamente.

La app móvil de Filarr (React Native) está en la recta final antes de su lanzamiento. Antes de que salga, la capa de sincronización tiene que ser honesta, no solo funcional: honesta. Cuando algo va mal, la app debe decir lo que sabe, solo lo que sabe, y dejar de fingir que va a arreglar lo que no puede arreglar.

Ese fue el tema de la semana.

Acabar con el eterno aviso «sync interrupted»

El fallo más visible en el móvil era un aviso que no desaparecía nunca: «sync interrupted» (sincronización interrumpida). Al revisar los registros del dispositivo encontré tres causas distintas, que se alimentaban entre sí en un bucle muy molesto.

Entradas fantasma en el manifiesto. Un archivo existía en el manifiesto de sincronización con todos los metadatos correctos, pero su objeto en R2 había desaparecido. Un 404, confirmado. El ciclo de sincronización trataba ese 404 como un error de red pasajero («fetch-failed»), lo que significaba: lo vuelvo a intentar. Pero un 404 del servidor no es un fallo momentáneo. Es un hecho. El archivo no está.

El arreglo introduce un nuevo estado de error permanente: missing-remote-object. Solo se activa cuando la ausencia está demostrada (el servidor respondió 404, no un silencio ni un tiempo de espera agotado). El aviso desaparece. El archivo recibe una línea honesta en la bóveda. Sin purga: eliminar la entrada sería una afirmación que leerían los demás dispositivos, y yo no tengo nada que afirmar, solo algo que observar. También revisé el código de escritorio y confirmé que una entrada ausente en remoto pasa a subida, no a borrado, así que ninguna copia local en otro dispositivo corre peligro.

Sumas de comprobación obsoletas en el manifiesto. Otra causa del aviso eterno: una suma de comprobación que no coincidía. El manifiesto tenía una suma que ya no correspondía al objeto en R2, un resto de una migración en la que las entradas se escribieron con la suma del contenido cifrado y después el objeto se reescribió bajo la misma clave determinista. La comprobación de integridad hacía su trabajo, pero la discrepancia era permanente, no pasajera.

El arreglo recurre a la prueba criptográfica en lugar de rechazar. Los bytes ya están descargados (no hace falta un segundo viaje de ida y vuelta). Pasan por la verificación de la etiqueta AES-GCM con las claves candidatas. Si la superan, la entrada se repara: se actualizan la suma y el tamaño, en una operación compare-and-swap para que una republicación simultánea tenga la última palabra. Si fallan, se conserva exactamente el comportamiento anterior y no se concluye nada a partir de bytes no autenticados. La suma del manifiesto es una comprobación previa barata que escribimos nosotros. La etiqueta GCM es una prueba criptográfica del origen y de la integridad a la vez. Cuando no coinciden, manda la prueba. Si quieres ver cómo se apilan en la práctica las capas de cifrado de Filarr, ese artículo recorre toda la arquitectura defensiva.

Un bloqueo mutuo entre las entradas fantasma y la autoridad de sincronización. La tercera causa era un auténtico bloqueo mutuo. Purgar las entradas fantasma exigía la autoridad de sincronización. Obtener la autoridad de sincronización exigía un ciclo sin corrupción. Y la única «corrupción» que quedaba eran las propias entradas fantasma. El aviso mostraba sus tres caras (corrupción, transferencia interrumpida, entrada imposible de ubicar) y ninguna desaparecía.

El arreglo tiene tres partes. Un nuevo tipo de error permanente, unplaceable-entry, cubre las entradas que solo existen en remoto y no tienen ruta local. Es un defecto de esquema, no una violación de integridad. El aviso rojo de corrupción no se enciende. La autoridad vuelve a ser alcanzable. La migración publica ahora localPath en cada entrada de blob, así que no nacerán nuevas entradas imposibles de ubicar. Y las entradas redundantes se pueden purgar con autoridad cuando existe un duplicado demostrado en el mismo manifiesto, con una protección específica: dos gemelas imposibles de ubicar nunca se purgan entre sí.

Versionar la capacidad de lectura: los veredictos antiguos no sobreviven al lector

Aparte del bucle del aviso: catorce archivos de un perfil de escritorio seguían marcados como ilegibles en el móvil, incluso después de una actualización que añadía justo la capacidad de lectura que necesitaban. Ningún intento nuevo, ningún reintento. El ciclo repetía un veredicto guardado por una versión anterior de la app.

Guardar en caché los veredictos de no disponibilidad tiene sentido en principio: no vuelvas a descargar un archivo que fallará por el mismo motivo. Pero «esta versión de la app móvil no puede leer este archivo» es un juicio emitido por un lector concreto. Tiene que llevar su versión.

El veredicto guardado va ahora envuelto con READER_CAPABILITY_VERSION. Un veredicto de una versión anterior, o uno sin versión de antes de este cambio, se considera obsoleto y se reintenta una vez. La regla para incrementarlo: cada vez que se amplía la capacidad de lectura (una nueva familia de contenedores, un nuevo formato de transporte, una nueva clave candidata). No para correcciones de errores que no cambian nada de lo que se puede leer. Cada incremento cuesta una descarga por cada entrada que antes no estaba disponible, así que no es gratis, pero es honesto.

Pantalla de inicio: tu cuota de almacenamiento, ahora sí consultada

El bloque de uso de almacenamiento de la pantalla de inicio mostraba siempre «usage unavailable» (uso no disponible) en las cuentas en la nube. Todas y cada una de las veces.

La API estaba ahí. El worker devuelve storageUsed y storageLimit en GET /billing/status. El cliente tenía un getBillingStatus tipado. La lógica de visualización, resolveStorageBlock, sabía calcular el indicador y el umbral de alerta, con pruebas. Solo faltaba la llamada. HomeScreen llamaba a resolveStorageBlock({ accountMode, tier }) sin pasarle nunca los bytes. La etiqueta decía la verdad, sin querer y de forma indefinida.

La cuota se consulta ahora al montar la pantalla, al cambiar de modo de cuenta y al final de cada ciclo de sincronización completo. Cambia cuando subes archivos, así que una cifra congelada en el montaje quedaría desfasada delante de tus ojos. Es un intento sin garantía y nunca bloquea: sin conexión o si el servidor no responde, el bloque vuelve honestamente a «no disponible». Una cifra de hace tres días presentada como actual sería peor que no tener cifra. Ninguna llamada en las cuentas locales: ahí el bloque no se muestra.

Encima de eso hay una ventana de frescura de dos minutos. Medido en el dispositivo antes del arreglo: siete llamadas a /billing/status en veinte segundos, porque la pantalla de inicio se vuelve a montar cada vez que regresas a la pestaña. El gesto que mueve la cuota (deslizar hacia abajo para actualizar) fuerza ahora una lectura inmediata. Dos minutos entre lecturas automáticas: lo bastante corto para que, al volver al inicio tras una importación, veas la cifra correcta, y lo bastante largo para que la navegación normal no cueste nada. La marca de tiempo solo se fija cuando la llamada tiene éxito, así que una caída pasajera no condena el bloque a «no disponible» durante toda la ventana.

Para entender cómo la sincronización en la nube opcional sigue siendo de conocimiento cero mientras corre toda esta lógica, ese artículo explica lo que viaja realmente al servidor: blobs cifrados, opacos para nosotros.

3326 pruebas, todas en verde

Cada arreglo de esta semana salió con sus pruebas. La batería de errores de sincronización cubre la detección de missing-remote-object en las dos formas de respuesta de axios (envuelta y en bruto), el versionado de la capacidad de lectura y la adopción de la caché heredada, la degradación de las sumas obsoletas con el recurso a GCM, el arreglo del bloqueo mutuo con la protección de las gemelas imposibles de ubicar, y el comportamiento de los ciclos de carpetas ante un 404. La paridad de la internacionalización está en 1477/1477 cadenas.

Una novedad que merece mención: al arrancar, ahora se escribe un marcador de compilación en el registro de diagnóstico. Parece trivial. No lo es. Una sesión de diagnóstico perdida me costó una vez tiempo real de depuración porque no podía confirmar que el dispositivo ejecutaba el código que yo creía. El marcador cierra ese hueco.

Lo que viene

La capa de sincronización está ya en un punto en el que la llamaría fiable. Lo siguiente en el móvil:

  • Una pasada de pulido de la interfaz: la lista de bóvedas, el editor y el flujo de bienvenida todavía tienen asperezas de tanto iterar deprisa
  • Las pantallas de bienvenida: las de derivación de claves y de frase de recuperación necesitan una última pasada de diseño antes de que las vea cualquier usuario nuevo (el modelo de cifrado con las claves KEK y FEK que configuran es el mismo que ya funciona hoy en el escritorio)
  • Pruebas de extremo a extremo en un dispositivo real durante todo el ciclo de sincronización: subir desde el escritorio, leer en el móvil, editar en el móvil y resolver el conflicto de vuelta en el escritorio
  • Una pequeña versión beta para los primeros testers

Si quieres seguir el desarrollo o tener acceso anticipado, el artículo del hito de los 1000 usuarios da la visión completa de dónde estamos y de lo que viene después del lanzamiento móvil. El código fuente del cliente de escritorio ya es público, con la licencia BSL 1.1, y la app móvil hereda la misma arquitectura de cifrado.

Faltan uno o dos meses, aproximadamente. Desarrollando en público todo el camino.