5 puntos por GN⁺ 2023-12-02 | 1 comentarios | Compartir por WhatsApp
  • Las apps frontend complejas parten de una caché de respuestas de API y terminan asumiendo índices manuales e invalidación de caché, hasta quedar cerca de reimplementar una pequeña base de datos en cada proyecto
  • En frameworks declarativos como React, para no llamar a la API en cada render se coloca una caché en el estado local o en una capa como Redux, y esa capa poco a poco termina cumpliendo el papel de almacén central
  • Una estructura de almacenamiento por ID y otra de consulta por fecha aceleran las búsquedas, pero mantener la consistencia entre varias estructuras como CACHE y ENTRIES_BY_DATE aumenta la carga en pruebas y revisiones de código
  • Los cambios optimistas actualizan la UI de inmediato antes de la respuesta del servidor para mejorar la sensación de velocidad, pero traen un costo de consistencia: duplicación de lógica entre cliente y servidor, seguimiento de cambios en curso, rollback ante errores y reconciliación tras reiniciar la app
  • SQLSync busca ofrecer dentro del stack frontend una caché duradera, índices, restricciones, cambios optimistas y consultas reactivas mediante una base de datos local basada en SQLite y sincronización similar a Git Rebase

Cómo la caché del frontend crece hasta convertirse en una base de datos

  • La gestión de datos en frontend puede comenzar con una simple caché que guarda respuestas de API en variables locales
    • Los frameworks declarativos como React vuelven a renderizar el árbol varias veces durante la interacción del usuario
    • Para no enviar una solicitud a la API en cada render, se pueden guardar en el estado del componente los resultados o errores de la petición con useState y useEffect
    • El ejemplo está simplificado para dar claridad; en la práctica también existe la opción de usar librerías de API ya probadas
  • La caché puede moverse a un nivel más alto del árbol de UI o incluso fuera de la UI
    • Redux es una librería de gestión de estado para React que unifica el estado y coordina cambios atómicos a lo largo del tiempo
    • El ecosistema de Redux se ha expandido con herramientas y patrones para gestionar la caché de datos de API
    • Este modo de uso busca centralizar la lógica de caché, coordinar actualizaciones y compartir resultados en caché entre componentes
  • A medida que la capa de caché crece, se parece cada vez más a un sistema de almacenamiento central que maneja datos de forma eficiente según el motor de renderizado y las acciones del usuario

Índices manuales y la carga de mantener consistencia

  • Incluso en el frontend se pueden guardar los datos recibidos del servidor en objetos cuya clave sea el ID, para consultarlos y modificarlos rápidamente
    • En apps que usan REST API es común leer datos en lote y luego enriquecer objetos específicos según se necesite
    • Guardar los objetos por ID facilita fusionar resultados de la API dentro de la caché
    • Esta estructura está optimizada para crear, leer, actualizar y eliminar por ID
  • Los filtros que requieren recorrer varios elementos suelen llevar a crear índices separados para evitar revisar todas las entradas
    • Si se construye una estructura ENTRIES_BY_DATE usando las partes de año/mes/día de createdAt, se pueden encontrar rápido las entradas de una fecha concreta
    • Pero a cambio hay que mantener constantemente la consistencia entre CACHE y ENTRIES_BY_DATE
    • Las consultas por rango de fechas requieren múltiples accesos, y una estructura con arreglos ordenados por fecha y lógica de consulta/actualización más compleja podría ser mejor
  • Cuantos más índices haya, más lógica de creación, actualización y consulta se necesita para cada uno
    • Verificar que todo sea correcto aumenta la carga en pruebas y revisiones de código
    • Si se omite un borrado o una actualización en un índice, pueden aparecer bugs difíciles de detectar
    • Puede terminar yéndose más tiempo en esta infraestructura de complejidad que en nuevas funciones de la aplicación
  • Los índices de una base de datos real son mucho más complejos que simplemente guardar los datos con otra forma
    • Incluyen elementos como recopilación de estadísticas, control de versiones de datos, control transaccional, locks e interacción con la optimización de consultas

Los problemas de consistencia que agregan los cambios optimistas

  • Un cambio optimista simula localmente el efecto de una operación antes de recibir la respuesta del servidor
    • La UI puede responder al instante como si no existiera latencia de red
    • Si el servidor toma una decisión distinta a la esperada o ocurre un error, la UI puede tener que revertir el cambio y pedir al usuario que corrija el problema
    • Es una herramienta poderosa cuando el cliente puede predecir bien el resultado del servidor, manejar errores localmente y mantener la lógica estrechamente sincronizada
  • Las actualizaciones optimistas suelen seguir cuatro pasos
    • La UI dispara una operación de escritura
    • Se aplica el cambio en la caché local asumiendo que el servidor estará de acuerdo, y la UI se vuelve a renderizar de inmediato
    • La operación de cambio se envía al servidor de forma asíncrona
    • La respuesta del servidor se fusiona con la caché local para sobrescribir el cambio optimista previo y, si hace falta, volver a renderizar la UI
  • Mantener la consistencia con el servidor trae varias cargas
    • Hay que duplicar lógica en cliente y servidor para poder predecir resultados
    • Para manejar errores asíncronos o discrepancias con el servidor, hay que rastrear cada cambio en curso
    • Para ofrecer una mejor experiencia, puede ser necesario hacer persistente la parte optimista de la caché para reconciliar cambios después de reiniciar la app
  • Este proceso aumenta el tiempo de desarrollo y el costo de verificar corrección, y puede hacer que la gestión de datos se adelante al desarrollo de valor para el usuario o de funciones diferenciadoras

La complejidad de la invalidación recursiva de caché

  • En apps con muchos datos, la misma información aparece en varios lugares de la caché
    • La caché del ejemplo guarda projects, tasks y users juntos
    • Tras completar una tarea, pueden verse afectadas varias partes como el progreso del proyecto, las tareas asignadas a un usuario y la información de nuevas tareas
  • Para alinear la caché con el servidor después de completar una tarea, pueden hacer falta varios viajes de ida y vuelta
    • Avisar al servidor que la tarea fue completada
    • Refrescar el proyecto porque cambió su progreso
    • Verificar si hay nuevas tareas asignadas
    • Si hay una nueva asignación, consultar esa tarea
  • Se pueden reducir esos viajes con APIs más complejas, pero eso deja a la API o a la lógica del cliente acopladas al modelo de datos subyacente
    • GraphQL es un enfoque para este problema, pero no una solución completa
  • Una estructura donde la UI debe saber qué partes de la caché están relacionadas con cada cambio se vuelve frágil a medida que escala
    • Las relaciones y agregaciones de datos pueden afectar múltiples partes de la caché local
    • Cuando el equipo de ingeniería crece, el problema puede cruzar fronteras entre equipos y sentirse parecido a las variables globales mutables en grandes proyectos de software
  • Al combinarse con cambios optimistas, el cliente termina replicando aún más lógica de backend para predecir cambios del servidor
    • En el ejemplo, podría intentarse quitar la task 1 del user 1 y recalcular el progreso como una nueva proporción usando el total de tareas
    • Cuanto más se intenta predecir localmente cambios anidados, más duplica el cliente el stack de backend

El stack de base de datos frontend que propone SQLSync

  • SQLSync es un stack de base de datos optimizado para frontend construido sobre SQLite
    • Su motor de sincronización se basa en ideas de Git y de sistemas distribuidos
    • Está diseñado para integrarse de forma fluida con frameworks frontend populares como React, Vue y Next.js
    • Su objetivo es resolver los problemas difíciles de gestión de datos para que los desarrolladores se concentren en las funciones propias de la aplicación
  • La Todo app de ejemplo implementa toda la capa de datos con 60 líneas de Rust y unas cuantas consultas SQL distribuidas entre componentes
    • SQLSync ofrece caché duradera, índices/restricciones/triggers/optimización de consultas de SQLite, cambios optimistas, invalidación inteligente de caché y consultas reactivas
  • Los datos locales se almacenan en una o más bases de datos SQLite
    • Se pueden crear índices fácilmente y estos se sincronizan automáticamente con los datos
    • La base de datos puede usar automáticamente índices, como en el backend, para acelerar consultas
    • SQL permite expresar consultas complejas y también usar funciones como triggers, foreign keys, constraints y full-text search
  • Los cambios optimistas se manejan con un reducer
    • La estructura es similar a los conceptos centrales de Redux
    • El reducer puede escribirse en cualquier lenguaje que pueda compilarse a WebAssembly
    • SQLSync ejecuta los cambios de forma optimista en el cliente y, en el servidor, los ejecuta en un orden globalmente consistente
    • Después, el cliente se sincroniza con el servidor mediante una operación similar a Git Rebase
  • Esta arquitectura tiene la ventaja de eliminar la necesidad de invalidación recursiva de caché
    • Toda la lógica de cambio de datos se escribe en un reducer fácil de compartir entre cliente y servidor
    • Todos los cambios de datos producidos durante una modificación se vuelven visibles automáticamente
    • Como la sincronización funciona como Git Rebase, incluso si el servidor aplica cambios distintos a los del cliente, el cliente tiene la garantía de llegar al mismo resultado consistente

Trabajo relacionado

  • “Building data-centric apps with a reactive relational database” de Riffle aborda la idea de guardar todo el estado de la aplicación, incluido el estado de UI, en una sola base de datos reactiva
    • Las consultas reactivas ofrecen un modelo mental limpio y encajan bien con sistemas declarativos como React
    • Resuelve problemas del desarrollo de apps cliente con ideas provenientes de la comunidad de bases de datos
    • Trata las ventajas de modelar estado con un modelo de datos relacional e índices reales
  • Stepan, de Instant.db, escribió dos textos sobre bases de datos dentro del navegador
  • CR-SQLite de Matt Wonlaw es una extensión de SQLite
    • Usa tipos de datos replicados sin conflicto (CRDT) y un log de eventos con orden causal para fusionar datos de manera consistente
    • Permite que apps peer-to-peer guarden datos en SQLite y colaboren sin un coordinador central
    • También es un ejemplo de ejecutar SQLite en el navegador
  • Matt Wonlaw también está explorando ideas relacionadas

1 comentarios

 
GN⁺ 2023-12-02
Opiniones de Hacker News
  • Conozco bien este proyecto, y como quien lo hizo es amigo mío, voy a intentar que venga aquí a responder preguntas.
    Es un arquitecto de bases de datos experimentado. Con SQLsync hizo posible que los desarrolladores frontend consulten y actualicen una base de datos remota como si estuviera completamente dentro del navegador. En realidad, casi es así: gracias a WASM, se puede enviar toda la base de datos SQLite al navegador. La clave está en un algoritmo reactivo inteligente pero simple que sincroniza entre varios clientes.
    Si consideramos que una parte importante del trabajo de desarrollo es la sincronización de datos, React y las API REST también pueden verse como una especie de procedimiento de sincronización, y este enfoque abre nuevas posibilidades. Ya no hace falta crear otra base de datos rara y personalizada con un árbol de objetos traído desde una API y cacheado; con el poder de una base de datos relacional, se puede actualizar y consultar directamente en local.

    • Creo que mover todo el sistema de consultas al frontend es el punto al que muchos desarrolladores frontend realmente quieren llegar. Quieren un sistema de consultas potente sobre los datos, no seguir reinventando la capa de transporte, REST, GraphQL, *RPC y cosas por el estilo.
      Sin embargo, en las empresas web tradicionales es difícil adoptarlo por la especialización de los equipos de backend/frontend. Es como quitar las capas de base de datos, backend, transporte y autenticación, y reemplazarlas por un único sistema en bloque; además, la mayoría de los arquitectos de sistemas vienen del backend, así que no entienden bien este problema. Como toca profundamente ambos lados, no encaja bien en sistemas existentes y termina sirviendo más para desarrollos nuevos. El backend no es un servicio de AWS o Azure ni es amigable con Lambda, así que la mayoría de los tipos de arquitectos con los que me cruzo no querrían meterse con esto.
      Este enfoque ya existe en cierta medida con una tecnología anterior: CouchDB+PouchDB. Para algunos usos encaja bastante bien, pero el sistema de consultas no es ideal, y la autenticación y la forma de delimitar el alcance de los datos resultan poco familiares para la mayoría. Lo más fácil es cuando los datos pertenecen por completo a un único usuario y se usa tal cual un modelo de base de datos por usuario. Si se particionan fuertemente los datos con CRDT, también se reducen mucho los problemas de conflictos.
      Pero hay problemas de escalabilidad. Cuando CouchDB tiene entre 10 mil y 100 mil usuarios conectados, sus requisitos de CPU son muy altos; la tecnología también es antigua, aunque se mantiene. Desde el punto de vista del diseño de sistemas, cuando se empieza a compartir datos entre usuarios la complejidad aumenta rápidamente, y en lugar de resolverla solo se la mueve de lugar, por lo que deja de encajar tan bien.
      Este enfoque parece apuntar al mismo objetivo, pero probablemente sufra problemas de escalabilidad similares. Tengo curiosidad por ver cómo evoluciona; parece un primer paso.
    • Esto suena bastante parecido a Couchbase, que permite consultar/actualizar una base de datos que se sincroniza de forma remota y luego con peers. También se puede controlar fácilmente la autenticación o la lógica de negocio con plugins de JavaScript del lado del servidor.
    • Por pura curiosidad, ¿por qué no cachear solo las partes relevantes en LocalStorage / SessionStorage?
      Recuerdo que antes Chrome intentó meter literalmente una base de datos SQL en el navegador, pero no salió bien y localStorage se volvió lo dominante. No lo digo para desmerecer su utilidad; normalmente uno tiende a elegir lo que ofrece el navegador. Me entusiasma mucho la posibilidad de traer esto al navegador con WASM y a medida que eso madure o gane más funciones.
  • En una empresa donde trabajé antes usábamos software de gestión de proyectos con un mecanismo de check-out/check-in para los cambios. Al hacer check-out de un proyecto, descargabas una copia para modificarla en local; al hacer check-in, la subías de nuevo al servidor. Mientras estaba en check-out, el proyecto quedaba bloqueado. En la era de las apps con actualizaciones en vivo, a todos nos parecía un método anticuado.
    Pero después de 10 años haciendo webapps SPA, esa forma de sincronizar datos ahora me parece adelantada a su tiempo.

    • Que se admitan actualizaciones en vivo en paralelo de varios usuarios, o que se bloquee para que solo haya una actualización a la vez, en última instancia no es una decisión técnica sino de negocio. La clave es si las reglas de negocio adecuadas para la aplicación permiten manejar actualizaciones en vivo concurrentes.
      Al final, todo se reduce a si se puede implementar un procedimiento que resuelva de manera consistente las discrepancias entre varias actualizaciones simultáneas. A veces se puede y a veces no; depende más de las reglas de negocio que de la capacidad técnica.
      Si por reglas de negocio no se puede implementar un mecanismo de resolución, entonces aunque exista la capacidad técnica para soportar actualizaciones concurrentes, hace falta un bloqueo para permitir solo una actualización a la vez.
    • Si se va por este camino, se resuelven muchos problemas y la implementación se vuelve muy fácil.
      Pero es difícil convencer a otros de que esto es lo que realmente se quiere. Es fácil caer en la gran ilusión de que todo debe estar siempre disponible, pero en la realidad normalmente una persona hace cambios a la vez; y si dos o más personas tienen que trabajar, de todos modos muchas veces tienen que hablar o comunicarse para coordinarse.
      Incluso en un desarrollo completamente distribuido como Git, los conflictos no se pueden resolver automáticamente por arte de magia. Para elegir el cambio correcto, todavía hay que comunicarse con otras personas y entender el contexto.
    • Precisamente por esto, aunque la gente hable por debajo del paradigma clásico de bases de datos relacionales como MySQL frente a bases de datos no relacionales como NoSQL o MongoDB, sigue sobreviviendo. No se puede reemplazar todo solo porque algo sea rápido o esté de moda.
      Algunas cosas necesitan soluciones probadas.
    • Suena a RCS: https://en.wikipedia.org/wiki/Revision_Control_System
      Recuerdo que cuando una empresa donde trabajaba pasó de RCS a CVS, uno de mis colegas se molestó porque CVS no soportaba check-out con bloqueo.
      https://en.wikipedia.org/wiki/Concurrent_Versions_System
    • Me gusta este enfoque. SQLSync en realidad hace esto continuamente, pero si se coordina explícitamente también debería ser posible ese esquema de check-in/check-out.
      Creo que una estrategia de bloqueo con un único propietario también se puede simular con SQLSync. Aunque, según la app, quizá no sea necesario. Si el objetivo es hacer trabajo sin conexión y fusionar cuando esté listo, SQLSync ofrece este patrón de forma nativa. Si el objetivo es que solo un cliente pueda hacer cambios, se necesita un patrón de bloqueo central, y es posible que eso también pueda coordinarse mediante SQLSync.
  • Aquí se entrelazan el principio de “lo que se mide se gestiona” y la falacia del costo hundido.
    El verdadero problema de una base de datos es la complejidad. Cada funcionalidad individual suele ser segura, pero cuando estabilidad, caché e índices empiezan a interactuar, la complejidad explota, y normalmente no tiene sentido implementar una base de datos específica del dominio.
    Pero para cuando una empresa se da cuenta de que ya invirtió y volcó muchos recursos en implementar esas tres funcionalidades, políticamente es difícil recomendar quitarlas, y el costo real de eliminar la deuda técnica de una sola vez también es grande.
    Creo que el verdadero problema es la sintaxis de SQL. Si la experiencia de usar una base de datos relacional básica fuera tan cómoda como una sintaxis familiar estilo C, en vez de un inglés roto, habría más incentivos para usar una base de datos que para crear una propia. Las bases de datos NoSQL fueron un buen paso en esa dirección, pero en general se enfocaron demasiado en big data por encima de la utilidad cotidiana. Algo como Redis se consolidó y está bien.
    Hacer que SQL sea fácil de ejecutar es un enfoque razonable, pero en buenas bases de datos, por ejemplo Postgres, que me gusta, SQL es el lenguaje predeterminado, así que es difícil obtener eficiencia sin usar ese lenguaje. Realmente hace falta una base de datos tipo PostgresPostSQL que replique perfectamente Postgres, pero cuyo parser predeterminado soporte un lenguaje con buena sintaxis.

    • No sé exactamente qué tiene de difícil SQL. Creo que todos los desarrolladores deberían saber SQL. La sintaxis de SQL también es buena y está lo bastante probada como para haber sobrevivido tanto tiempo. En vez de criticarlo, sería más útil invertir tiempo en aprenderlo de verdad.
    • SQL recibe críticas con frecuencia, y creo que hay razones para ello, pero ¿por qué no hemos logrado crear algo mejor?
      En la programación general se usan decenas de lenguajes y estos siguen evolucionando. Incluso JavaScript, que es difícil de cambiar porque lo ejecuta el navegador y no se puede controlar el navegador del usuario, está evolucionando mediante transpiladores y WebAssembly.
      Pero en bases de datos, en la práctica solo existe SQL. Hay alternativas, pero ninguna se acerca a SQL en uso. Tal vez SQL no sea tan malo.
      La razón podría ser que el modelo relacional es realmente bueno. Los intentos de apartarse de él probablemente solo funcionen en nichos. El estilo declarativo también es muy bueno, y es difícil lograr un gran éxito alejándose de él. Si al final solo se termina creando un SQL con otra sintaxis, para la mayoría no será una mejora lo bastante grande como para cambiar de enfoque.
    • Ojalá Postgres tuviera una API más estable y de más bajo nivel que SQL. Podría tener una forma parecida al plan de consulta que se obtiene con EXPLAIN.
      Una aplicación escrita contra esta API podría implementar una base de datos SQL. Bastaría con parsear SQL e implementar un planificador de consultas que emita planes de consulta compatibles con esta API.
    • Me pregunto si han considerado agregar una capa semántica sobre la base de datos para quienes quieren evitar escribir SQL directamente.
  • Soy el autor. Apenas pude revisar la mayoría de las preguntas, y seguiré comprobando periódicamente si se me escapó alguna. También me da curiosidad si alguien creó una mejor forma de seguir las discusiones de HN.
    Me alegra mucho la discusión hasta ahora. El primer artículo se centró más en la motivación de ingeniería frontend que me llevó a crear SQLSync que en cómo funciona SQLSync concretamente. En el próximo artículo voy a cubrir cómo funciona.

    • Me pregunto si hay planes de soporte para móviles. Ahí es exactamente donde más me gustaría probar esto.
  • No hay que darle al usuario un modelo mental que pueda romper la realidad de forma grave, o invisible.
    Me preocupa que sincronizar bases de datos en lugar del modelo cliente-servidor sea uno de esos casos. Puede que el mecanismo de sincronización simplemente se derrita, o que haya supuestos profundos que no se cumplen.
    Si hace falta una UI rápida, me parece más seguro crear y usar un conjunto de primitivas CRDT, y que el resto se mantenga como envío de formularios.

    • De acuerdo. Uno de los objetivos es que los desarrolladores que usen SQLSync puedan entender fácilmente el modelo mental. Tendré mis sesgos, pero personalmente siento que el modelo de rebase es mucho más fácil de entender que los CRDT.
  • La sincronización de estado entre cliente y servidor es un problema maldito.
    Si se acepta sacrificar un poco de experiencia de usuario y se vuelve a algo más cercano al modelo de PHP/renderizado del lado del servidor, se puede evitar todo este problema. Las SPA son buenas, pero el envío de formularios multipart todavía funciona. Con muy poco JavaScript también se pueden pulir la mayoría de las asperezas restantes.
    En los productos web recientes, el estado del lado del cliente consiste en los claims de autenticación de un IdP de terceros, el ID de sesión first-party en los parámetros de consulta y el documento actual. Sinceramente, ni yo sé dónde se guarda lo primero. Ese es problema de Microsoft, no nuestro. Todo el resto del estado está en el servidor.
    Tratamos al cliente como una terminal tonta que solo escupe entradas todo el día. No usamos cookies first-party ni almacenamiento local. Este enfoque mejoró mucho la experiencia de desarrollo para iOS/Safari.
    Por eso quiero preguntar cuál es la experiencia que realmente se quiere ofrecer, y por qué justifica separar el estado del cliente y del servidor.

    • En interfaces gráficas de consumo, el renderizado optimista es un flujo muy común, y si haces eso, ya estás lidiando con estado cliente/servidor. Todavía se ven spinners de carga por todos lados, pero normalmente son para la carga inicial de contenido. Por ejemplo, Gmail no te hace esperar cuando archiva un correo.
    • ElectricSQL logró grandes avances hacia la resolución de este problema. Permite escribir en SQLite en el cliente y garantiza la sincronización con Postgres.
      Referencia: https://news.ycombinator.com/item?id=37584049
  • Parece que lo offline/local-first basado en SQLite está muy de moda últimamente. Es la tercera vez que leo sobre esto esta semana, y se ve bien.
    Pero ¿cómo se compara con ElectricSQLhttps://electric-sql.com/ y PowerSynchttps://powersync.com/?

    • Sí, definitivamente es un área muy activa. Es muy interesante ver cómo aparecen distintos enfoques.
      ElectricSQL y PowerSync abordan ambos un problema muy difícil: la replicación parcial. Intentan crear una solución general en la que una base de datos central tradicional sincroniza en ambos sentidos solo lo que el cliente necesita, a la vez que admite cambios optimistas y el manejo de consistencia/conflictos que eso implica.
      La desventaja es la complejidad de implementación. Hay que rastrear con precisión qué subconjunto de toda la base de datos tiene cada cliente para poder enviar cambios solo a ese subconjunto. Además, para especificar qué subconjunto del estado de la base de datos se va a descargar, se necesita un nuevo DSL, que también hay que aprender y optimizar. Aun así, me alegra que estén resolviendo un problema muy difícil, y cuando SQLSync esté listo para soportar replicación parcial, ya habrá buenas prácticas establecidas.
      En cambio, SQLSync actualmente solo soporta la sincronización de toda la BD. Todos los clientes ven una vista consistente de toda la base de datos. Uno podría preguntarse de inmediato si esto es una buena idea, y no encaja para algunas apps. Pero si pensamos en una app de finanzas personales, los objetivos principales son sincronización entre dispositivos, respaldo en la nube y capacidad offline, así que puede ser justamente lo deseado que toda la BD esté almacenada en todos los dispositivos. Un modelo de datos orientado a documentos como Airtable también puede ser un ejemplo. Si cada Airtable se trata como una base de datos separada, el cliente puede encargarse de gestionar qué tablas le importan.
      Al enfocarse en la sincronización de toda la BD, el motor de sincronización se vuelve mucho más simple que las soluciones que soportan replicación parcial. Una de esas ventajas es que el backend es muy ligero. La demo actual (https://sqlsync-todo.pages.dev) se ejecuta por completo dentro de Cloudflare Durable Objects usando muy poco almacenamiento y tiempo de CPU.
      Para que SQLSync haga posibles estos casos de uso todavía queda mucho por hacer, y sigue estando más cerca de un prototipo, pero las pruebas iniciales han sido muy positivas.
  • En apps multitenant grandes, cuando los datasets individuales son relativamente pequeños, varias veces he pensado: “¿y si simplemente enviamos la base de datos al cliente?”. No profundicé mucho porque parece un patrón de arquitectura maldito, lo bastante fuera de lo estándar. Me gustaría descubrir que estaba equivocado.

    • Una vez hice algo así en una app de conteo de calorías. Aunque la base de datos tenía cientos de miles de alimentos, la app ocupaba mucho menos espacio que la mayoría de las apps de medios o juegos.
    • Yo también me pregunto si esta forma está maldita. Hasta ahora ha sido mucho mejor de lo esperado. Una app como https://sqlsync-todo.pages.dev se vuelve trivial con este patrón.
      Falta mucho por hacer para demostrarlo bien, pero estoy bastante entusiasmado por seguir empujándolo y ver adónde llega.
    • La respuesta correcta es devolver la UI al servidor donde ya está la base de datos y enviar solo HTML al renderizador HTML del lado del cliente, es decir, al navegador web. Todo este artículo se parece más a “el frontend cruzó la línea, pasó el precipicio y cayó al mar”.
  • Esto parece uno de esos problemas que desaparecen por completo si abandonas las SPA.
    Si usas soluciones de la familia Hotwire o htmx, las consultas son simplemente consultas al servidor, y el problema de hacer rápidas esas consultas está mucho mejor entendido.

    • Esto no es solo un problema de sitios web. ¿También los ecosistemas móvil y de escritorio deberían moverse ampliamente hacia clientes ligeros, como los navegadores? ¿Apps simples como Apple Reminders o Google Tasks deberían congelar la GUI por latencia o problemas de conexión?
    • Hace poco usé htmx, y la complejidad que elimina y la productividad que resulta de eso son realmente absurdas. También es una gran bendición poder usar tal cual el stack que quieras.
      Lo usé con ocaml + web components y fue una experiencia de productividad 10/10. Solo necesitas una herramienta de build que compile más rápido que un parpadeo, y no hace falta cablear mapeos JSON entre frontend y backend, así que es realmente productivo.
    • Sinceramente, soluciones como Hotwire o Livewire no son tan inmediatas como una SPA.
      Personalmente prefiero InertiaJs https://inertiajs.com. Es una especie de sistema de router frontend que sincroniza estado con el servidor a la “vieja usanza”.
    • Es parecido a decir “no construyas webapps con mucha interacción” o “no operes servicios que necesiten más de una VM”. Es una postura absurda.
    • Creo que el problema desaparece no cuando dejamos las SPA, sino cuando dejamos atrás las funciones de alta interacción que suelen tener los productos agradables de usar.
      Esto es especialmente cierto para productos que deben funcionar incluso en regiones con internet inestable.
  • Ahora estoy escribiendo un artículo muy similar sobre las “bases de datos full-stack”. Trata el patrón en el que muchas apps vuelven a crear en el código del cliente frontend la lógica del backend y de la base de datos. La solución que recomendamos es elegir una base de datos que pueda ejecutarse tanto en el servidor como en el cliente, y sincronizar ambos lados.
    La razón por la que no usamos SQLite en nuestro producto es, francamente, que SQL no es la herramienta adecuada para consultar datos de aplicaciones. No encaja fácilmente con las estructuras de datos que se quieren en el código del cliente, y casi ninguna base de datos SQL ofrece una forma de suscribirse a cambios en las consultas sin hacer polling repetido de la consulta.
    Si te gusta la idea de tener una base de datos completa en el cliente y quieres una integración profunda con TypeScript/JavaScript, estaría bueno que vieras lo que estamos construyendo: https://github.com/aspen-cloud/triplit

    • En realidad, Postgres ofrece una excelente forma de hacer suscripción a cambios en tiempo real mediante WAL. También mantengo esta biblioteca open source:
      https://github.com/cpursley/walex
    • SQLite en realidad ofrece el mecanismo necesario para detectar cambios mediante update hooks. Es una lástima que muchos bindings de SQLite no lo expongan.
      Yo lo uso de una forma muy simple: cuando cambian los datos de una tabla subyacente, vuelvo a ejecutar automáticamente la consulta. No será tan eficiente como actualizar los resultados de forma incremental, pero como las consultas de SQLite suelen ser muy rápidas, no lo veo como un gran problema.
    • No estoy de acuerdo con que no encaje fácilmente con las estructuras de datos que se quieren en el código del cliente. La normalización de datos es importante para las aplicaciones frontend reactivas y, de hecho, es necesaria para mantener los datos actualizados. Todas las operaciones CRUD también se vuelven mucho más fáciles de manejar.
    • Con Realm Sync y Mongo + Kotlin Multiplatform se puede cubrir casi cualquier plataforma, como servidor, web, móvil y escritorio. Claro que tiene un costo. Me interesan de verdad las alternativas, y me pregunto si esto también entra en el artículo.