Planteamiento y contexto
El módulo node:sqlite de Node.js 24 expone APIs síncronas. DatabaseSync representa una conexión SQLite; createSession() rastrea los cambios después de que inicia la sesión; session.changeset() devuelve un changeset binario; y el destino puede llamar a applyChangeset() con un manejador de conflictos que omite, reemplaza o aborta. Diseña un protocolo de sincronización incremental entre un cliente offline y un servidor.
El problema radica en la propagación segura de los cambios de la base de datos: claves primarias, comprobaciones de valores anteriores, política de conflictos, idempotencia, límites de transacciones y permisos. Un changeset no es SQL ejecutable ni representa una fusión automática entre bases de datos.
Qué está evaluando el entrevistador
Una respuesta sólida separa la generación en el origen, la entrega confiable, la aplicación atómica en el destino y las decisiones de negocio ante conflictos. Espera preguntas sobre el ciclo de vida de Session, changeset frente a patchset, SQLITE_CHANGESET_DATA y tipos de conflicto relacionados, aplicación duplicada, evolución del esquema y cómo el trabajo síncrono de DatabaseSync afecta el bucle de eventos de Node.js.
“Serializar y sobrescribir” pierde las actualizaciones independientes del destino. Devolver siempre SQLITE_CHANGESET_REPLACE sobrescribe silenciosamente los datos de negocio y no ofrece ninguna garantía de corrección.
Preguntas para aclarar primero
Topología y autoridad
Determina si la sincronización es unidireccional, bidireccional o una agregación multicliente, y qué copia es la autoritativa para cada entidad. Si ambos lados editan una fila, utiliza una versión, un ID de dispositivo o un evento de negocio para arbitrar; la devolución de llamada predeterminada de SQLite no es una regla de negocio.
Ciclo de vida de versiones y esquema
Confirma que el origen y el destino compartan el esquema, las claves primarias y los tipos de columna. Los changesets dependen de la estructura de la tabla. Las migraciones deben completarse y portar una versión de protocolo antes de que se reproduzcan los changesets antiguos.
Latencia y límite de seguridad
Aclara el tamaño del payload, la ventana de reintentos, la duración offline y los campos sensibles. Los payloads binarios requieren integridad, autenticación, protección contra reenvíos (replay protection) y cifrado; no son SQL de confianza.
Respuesta en 30 segundos
“Asigno a cada lote un ID y un cursor de versión de origen. El origen crea una Session, confirma una transacción local y exporta un changeset. El servidor valida el esquema, la firma, el orden y la idempotencia, y luego llama a applyChangeset dentro de una transacción en el destino. El manejador de conflictos aborta de forma predeterminada y registra la fila, la columna y el motivo; omitir o reemplazar solo se permite mediante una política de negocio explícita. Un lote exitoso avanza el estado persistente, los fallos se reintentan con un backoff acotado y los lotes duplicados devuelven su resultado anterior. El trabajo síncrono pesado de SQLite se ejecuta detrás de un worker o una cola para no bloquear el bucle de eventos de Node.”
Solución paso a paso
Paso 1: Crear lotes trazables
Tras crear una Session, incluye un alcance de transacción local explícito en un lote. Registra batchId, dispositivo de origen, cursor inicial, versión del esquema, hash del changeset y hora de creación. Exporta el Uint8Array desde session.changeset(), luego cierra o reutiliza la Session para evitar que se acumule un historial ilimitado.
Paso 2: Elegir changeset o patchset
Un changeset incluye información del valor anterior, útil para comprobar si la fila de destino todavía coincide con las expectativas. Un patchset es más pequeño pero expone menos contexto del valor anterior. Elige según los requisitos de ancho de banda y auditoría; el tamaño del payload por sí solo no es motivo suficiente para perder diagnósticos.
Paso 3: Validar y entregar de forma idempotente
Usa TLS, identidad de dispositivo y una firma de payload. Valida tamaño, hash, versión de esquema y origen. Almacena un registro único indexado por batchId y cursor de origen; un duplicado de un lote ya aplicado devuelve el resultado registrado. Aplica los lotes posteriores en orden de cursor, colocando los lotes faltantes en una cola de espera en lugar de omitirlos.
Paso 4: Aplicar en una transacción y clasificar conflictos
Abre una transacción en el destino y llama a applyChangeset. El manejador de conflictos recopila la tabla, la clave primaria, el tipo de conflicto y el valor de destino en un búfer de auditoría. Devuelve SQLITE_CHANGESET_ABORT por defecto para que el lote se revierta atómicamente. Solo los campos aprobados por la política pueden devolver OMIT o REPLACE, y la decisión debe quedar en un registro reproducible.
Paso 5: Definir fusiones de negocio
Los datos fusionables a nivel de campo pueden usar marcas de tiempo, versiones monotónicas o unión de conjuntos. El dinero, el inventario y los permisos no son seguros para una fusión ciega; envíalos a una cola humana o a un evento de compensación. No mutes el changeset original después de una decisión. Genera un lote secundario que contenga el lote principal y el motivo de la decisión para preservar la cadena de auditoría.
Paso 6: Manejar tipos y precisión de enteros
Node.js y SQLite admiten diferentes conjuntos de tipos. Si un INTEGER de SQLite excede el rango de enteros seguros de JavaScript y readBigInts está deshabilitado, leerlo puede lanzar ERR_OUT_OF_RANGE. Estandariza en BigInt, cadenas o un rango explícito. Utiliza Uint8Array para valores BLOB y rechaza objetos arbitrarios.
Paso 7: Controlar el costo síncrono
Las llamadas de DatabaseSync se ejecutan de forma síncrona. Transacciones largas, changesets enormes o llamadas frecuentes a applyChangeset pueden bloquear el bucle de eventos. Mueve la sincronización a un worker, un proceso separado o una cola controlada; limita el payload y las filas por lote; monitorea la duración de la aplicación, los conflictos, las reversiones y la antigüedad de la cola.
Respuesta de ejemplo de alta calidad
Trato cada lote de sincronización como un evento inmutable. El dispositivo genera un ID, cursor, versión del esquema y hash; la Session cubre solo una transacción local confirmada; y el changeset viaja a través de un canal autenticado y protegido contra reenvíos. El servidor valida la estructura y el orden, desduplica mediante el ID de lote y llama a applyChangeset dentro de una transacción SQLite en el destino.
La función de devolución de llamada para conflictos aborta de forma predeterminada, revirtiendo atómicamente y registrando la clave primaria, el tipo de conflicto y el valor en el destino. El inventario, el dinero y los permisos se envían a una fusión de negocio o a una compensación en lugar de un reemplazo genérico; solo los campos explícitamente seguros pueden omitirse o reemplazarse. El estado persistente del lote avanza el cursor solo tras la confirmación. Como DatabaseSync bloquea de manera síncrona, ejecuto los lotes grandes en un worker y pruebo entregas duplicadas, lotes faltantes, migración de esquemas, desbordamiento de enteros y recuperación ante caídas del proceso.
Errores comunes
- Error: Sobrescribir la base de datos de destino en cada sincronización. → Por qué falla: Las actualizaciones independientes del destino desaparecen. → Solución: Enviar un changeset, comparar los valores anteriores y registrar la decisión.
- Error: Devolver
REPLACEante cualquier conflicto. → Por qué falla: Los datos de negocio se sobrescriben silenciosamente. → Solución: Abortar por defecto y autorizar el reemplazo por campo y por invariante. - Error: Generar un nuevo lote en cada reintento sin una clave de idempotencia. → Por qué falla: La aplicación duplicada o los saltos de cursor generan efectos secundarios repetidos. → Solución: Desduplicar con el ID de lote, el cursor de origen y el estado de la aplicación.
- Error: Aplicar un changeset enorme en el bucle de eventos principal. → Por qué falla: El trabajo síncrono de
DatabaseSyncbloquea el tráfico HTTP y las tareas programadas. → Solución: Aislarlo en un worker o cola y limitar el tamaño del lote.
Preguntas de seguimiento y respuestas
Pregunta de seguimiento 1: ¿Cuándo usarías un patchset?
Usa un patchset cuando el ancho de banda sea limitado y el destino tenga suficiente contexto, con necesidades moderadas de auditoría de conflictos. Usa un changeset cuando se requieran los valores anteriores para explicar conflictos o realizar fusiones auditables, aceptando un payload de mayor tamaño.
Pregunta de seguimiento 2: ¿Qué sucede si al esquema de destino le falta una columna?
Rechaza el lote con un error de compatibilidad; no permitas que la aplicación intente adivinar el mapeo. Ejecuta la migración de destino, verifica la versión del esquema y las claves primarias, y luego reprocesa. Si deben coexistir múltiples versiones, realiza la conversión según la versión del protocolo reteniendo el payload original.
Pregunta de seguimiento 3: ¿Cómo evitas efectos secundarios en el callback de conflicto?
El callback solo recopila datos estructurados del conflicto y devuelve una decisión constante. No llama a servicios externos ni muta otras tablas. Escribe los registros de auditoría o las notificaciones después de la confirmación para que una reversión no deje un estado externo inconsistente.
Pregunta de seguimiento 4: ¿Por qué no enviar SQL directamente?
El código SQL carece de los valores anteriores de origen, del contexto de esquema y de los límites del lote. Los reintentos no pueden determinar de manera confiable si ya se aplicó, y sentencias no autorizadas podrían llegar al destino. Los changesets generados por SQLite pueden procesar cada conflicto en el destino, lo que los convierte en una primitiva superior para la sincronización controlada.
Pregunta de seguimiento 5: ¿Cómo verificas la paridad de protocolo para enteros y BLOBs?
Crea fixtures multilenguaje para el entero seguro más grande, un entero fuera de rango, negativos, NULL, texto UTF-8 y payloads binarios. Registra el tipo de SQLite, las opciones de lectura/escritura de Node y la representación serializada para cada uno; luego, valida que los bytes y los valores coincidan antes y después de la reproducción.