HyperDB es una base de datos binaria, fragmentada (sharded) y orientada a documentos para Node.js. Su diseño se centra en el alto rendimiento, la ausencia de dependencias externas (Zero-dependency) y una experiencia de desarrollo fluida mediante el uso de Proxy de JavaScript.
Sirve para almacenar estructuras de datos complejas y profundas sin cargar todo el conjunto de datos en memoria RAM. HyperDB divide automáticamente los objetos anidados en archivos binarios separados, cargándolos bajo demanda (Lazy Loading) y gestionando la memoria mediante un sistema LRU (Least Recently Used).
- Desarrolladores de Node.js que requieren almacenamiento persistente local rápido.
- Proyectos que necesitan atomicidad en la escritura sin la complejidad de bases de datos SQL o NoSQL pesadas (como MongoDB).
- Sistemas embebidos o aplicaciones de escritorio (Electron) donde minimizar las dependencias es crítico.
- Fragmentación Automática (Sharding): Al guardar un objeto (
{}) dentro de la base de datos, HyperDB lo detecta y automáticamente lo separa en su propio archivo binario (.bin), referenciándolo en el índice padre. Esto permite manejar bases de datos de gran tamaño sin saturar la memoria. - API Transparente (Proxy): La interacción con la base de datos es idéntica a manipular un objeto JavaScript nativo (
db.data.usuario = "x"). No requiere métodosget()oset()explícitos para el uso básico. - Serialización V8: Utiliza el motor de serialización nativo de V8 (
v8.serialize/deserialize), lo que lo hace significativamente más rápido y compacto que JSON. - Escritura Atómica: Garantiza la integridad de los datos escribiendo primero en archivos temporales (
.tmp) y renombrándolos al finalizar. - Gestión de Memoria (LRU Cache): Incluye un gestor de memoria interno que descarga archivos poco utilizados cuando se supera un límite configurado (por defecto 20MB).
- Debouncing de Escritura: Agrupa múltiples operaciones de escritura en un corto periodo de tiempo para reducir el I/O en disco.
- Zero-dependency: No utiliza librerías de terceros, solo módulos nativos de Node.js (
fs,path,crypto,v8).
- Entorno: Exclusivo para Node.js (>= 18.0.0) debido al uso de
fs/promisesy sintaxis moderna. - Tipos de Datos: La fragmentación automática (creación de nuevos archivos) solo ocurre con objetos planos (
Plain Objects). Instancias de clases personalizadas se serializan pero no generan nuevos fragmentos automáticamente a menos que se configure explícitamente. - Consultas Avanzadas: No posee un motor de consultas integrado (como SQL
WHEREo Mongofind). Las búsquedas requieren recorrer el objeto o implementar índices manuales. - Sincronía Aparente: Aunque la API parece síncrona (asignación de variables), las escrituras en disco ocurren de forma asíncrona y diferida (debounced). Si el proceso de Node.js se mata abruptamente (SIGKILL) antes del flush, podrían perderse los últimos cambios en memoria (aunque el método
flush()mitiga esto).
- Node.js v18.0.0 o superior.
Dado que es un paquete local o git, se instala vía:
npm install git+https://github.com/Syllkom/HyperDB.git
# O si tienes los archivos locales:
npm install ./ruta-a-hyper-dbPara iniciar la base de datos, se debe instanciar la clase HyperDB.
import { HyperDB } from 'hyper-db';
const db = new HyperDB({
folder: './mi_base_de_datos', // Carpeta donde se guardarán los archivos
memory: 50, // Límite de caché en MB
depth: 2, // Profundidad de carpetas para el sharding
index: {
threshold: 10, // Operaciones antes de forzar guardado del índice
debounce: 5000 // Tiempo de espera para guardar índice (ms)
},
nodes: {
threshold: 5, // Operaciones antes de forzar guardado de nodos
debounce: 3000 // Tiempo de espera para guardar nodos (ms)
}
});HyperDB organiza los datos en una estructura jerárquica para evitar saturar un solo directorio.
./mi_base_de_datos/
├── index.bin # Índice maestro (Entry point)
├── root.bin # Datos de la raíz
└── data/ # Almacenamiento fragmentado
├── A1/ # Carpetas generadas por ID (según profundidad)
│ └── B2C3.bin # Archivo binario con datos de un sub-objeto
└── ...
graph TD
User[Usuario / App] -->|Operación get/set| Proxy[HyperDB Proxy]
Proxy -->|Intercepta| Cluster[Cluster Manager]
subgraph "Lógica de Cluster"
Cluster -->|Es primitivo?| IndexData[Memoria Local]
Cluster -->|Es Objeto nuevo?| Shard[Shard Logic]
end
Shard -->|Genera ID| ID_Gen[Crypto ID]
Shard -->|Serializa| V8[V8 Serializer]
IndexData -->|Debounce Timer| FileMan[File Manager]
Shard -->|Write| Disk[Disk I/O]
FileMan -->|Persiste| Disk
Disk -->|Cache| Memory[Memory LRU]
Disk -->|File System| FS[fs / fs.promises]
Este ejemplo muestra cómo guardar datos simples y cómo HyperDB los persiste.
import { HyperDB } from 'hyper-db';
const db = new HyperDB({ folder: './db' });
// 1. Asignación directa (SET)
// Esto escribe en memoria inmediatamente y programa la escritura en disco.
db.data.nombre = "HyperDB";
db.data.version = 1.0;
// 2. Lectura (GET)
console.log(db.data.nombre); // Salida: "HyperDB"Al asignar un objeto, HyperDB crea un archivo separado. Esto es útil para listas de usuarios o datos masivos.
// Al asignar un objeto, se crea un nuevo archivo binario en ./db/data/...
// El índice principal solo guardará una referencia: { "configuracion": { "$file": "xx/xxxx.bin" } }
db.data.configuracion = {
tema: "oscuro",
notificaciones: true,
limites: {
diario: 100
}
};
// Acceso transparente (Lazy Loading)
// Al acceder a .configuracion, HyperDB carga el archivo correspondiente automáticamente.
console.log(db.data.configuracion.tema); Cómo asegurar que los datos se guarden antes de cerrar la aplicación.
async function cerrarApp() {
console.log("Guardando datos...");
// Fuerza la escritura de todos los procesos pendientes en el Pipe
await db.flush();
console.log("Datos guardados. Saliendo.");
process.exit(0);
}Uso avanzado de la lógica de "Flow" para interceptar o manejar rutas específicas.
// Obtener estadísticas de uso de memoria
console.log(db.memory());
// Salida: { used: "1.20 MB", limit: "20.00 MB", items: 5 }
// Eliminar una propiedad (y su archivo asociado si era un shard)
delete db.data.configuracion;
// Esto elimina la referencia y, tras el proceso de 'prune' o limpieza, el archivo físico.Aunque la interacción principal es vía Proxy, estas clases componen el sistema:
| Clase | Descripción |
|---|---|
| HyperDB | Fachada principal. Configura inyecciones de dependencias y expone this.data. |
| Disk | Maneja I/O. Posee métodos read, write, remove. Usa colas (Pipe) para evitar colisiones de escritura. |
| Memory | Caché LRU. Evita leer del disco si el objeto ya está cargado. |
| Shard | Lógica de fragmentación. Decide cuándo y dónde crear nuevos archivos binarios (forge) y limpia referencias (purge). |
| Cluster | Coordina el índice y los datos. Intercepta las escrituras para decidir si van al archivo actual o a un nuevo shard. |
| Flow | Utilidad para manejar estructuras de árbol y referencias a Proxies activos. |
El constructor de HyperDB acepta un objeto de opciones complejo que permite afinar el comportamiento de la base de datos, desde la ubicación de los archivos hasta la agresividad del sistema de caché y escritura.
const options = {
folder: './data', // Ruta base del almacenamiento
memory: 20, // Límite de memoria en MB
depth: 2, // Profundidad de subcarpetas para sharding
// Configuración del Índice Principal (index.bin)
index: {
threshold: 10, // Operaciones antes de escritura forzada
debounce: 5000 // Tiempo de espera (ms) para escritura diferida
},
// Configuración de Nodos/Fragmentos (archivos .bin individuales)
nodes: {
threshold: 5, // Operaciones antes de escritura forzada
debounce: 3000 // Tiempo de espera (ms) para escritura diferida
},
// Inyección de Dependencias (Avanzado)
$class: {
Disk: null, // Clase o instancia personalizada para I/O
Index: null, // Clase o instancia para gestión del índice
Flow: null, // Clase o instancia para flujo de navegación
Cluster: null, // Clase o instancia para gestión de clusters
Shard: null, // Clase o instancia para lógica de sharding
Memory: null // Clase o instancia para gestión de RAM
}
};folder: Define dónde se crearán los archivos. Si no existe, la claseDiskintentará crearla recursivamente.memory: Define el "techo suave" (soft limit) del caché LRU en Megabytes. Si los objetos cargados superan este tamaño, los menos usados se eliminan de la RAM.depth: Controla cómo se generan los IDs de los shards.depth: 0: Todos los archivos se guardan planos en la carpetadata.depth: 2: Se crea una estructura de carpetas basada en los primeros 2 caracteres del hash (ej.data/A1/A1B2...bin). Esto evita límites del sistema de archivos en directorios con miles de archivos.
threshold(Umbral): Número de operaciones de escritura (set/delete) que ocurren en memoria antes de forzar una escritura física en disco.debounce(Retardo): Milisegundos que el sistema espera tras una escritura en memoria antes de guardar en disco si no se alcanza el umbral. Reinicia el temporizador con cada nueva operación.
HyperDB implementa su propio sistema de gestión de archivos y memoria para garantizar rendimiento y consistencia.
Es el motor de persistencia. Sus responsabilidades clave son:
-
Atomicidad (
write/writeSync): Nunca sobrescribe un archivo directamente.- Serializa los datos con
v8. - Escribe en un archivo temporal (
filename.bin.tmp). - Fuerza el volcado al disco físico (
fsync). - Renombra el temporal al nombre final. Esto previene corrupción de datos si el proceso falla a mitad de escritura.
- Serializa los datos con
-
Cola de Promesas (
Pipe): Para evitar condiciones de carrera en operaciones asíncronas sobre el mismo archivo,Diskutiliza unMapllamadoPipe.- Si se solicitan 3 escrituras seguidas al mismo archivo, se encadenan en una promesa secuencial (
.then().then()).
- Si se solicitan 3 escrituras seguidas al mismo archivo, se encadenan en una promesa secuencial (
-
Mantenimiento (
prune): Escanea recursivamente los directorios de datos. Si encuentra carpetas vacías (remanentes de shards eliminados), las borra para mantener el sistema de archivos limpio.
Implementa un caché LRU (Least Recently Used) personalizado.
- Cálculo de Tamaño: Usa
v8.serialize(data).lengthpara estimar el peso real en bytes de los objetos. - Pinned Keys: Ciertas claves críticas (como
index.binyroot.bin) pueden marcarse como "pinned" para que nunca sean desalojadas del caché, garantizando que la estructura base siempre esté disponible. - Eviction Policy: Cuando
currentSize + dataSize > limit, elimina el elemento más antiguo (cache.keys().next().value) hasta tener espacio.
El corazón de la capacidad de escalar de HyperDB reside en la interacción entre Cluster.js y Shard.js.
Cuando se asigna un objeto a una propiedad:
- Detección:
Clusterdetecta que el valor es un objeto plano (Plain Object). - Delegación: Llama a
Shard.forge(index, value). - Recursividad:
Shardrecorre el objeto. Si encuentra sub-objetos anidados que también son shardables, los separa recursivamente. - Generación de ID:
Shardgenera un ID aleatorio hexadecimal (ej.A1B2C3D4) y calcula su ruta basada en ladepth. - Persistencia: Escribe el contenido del objeto en el archivo
.bingenerado. - Referencia: Devuelve un objeto "puntero":
{ $file: "A1/A1B2C3D4.bin" }. - Actualización del Padre: El objeto padre guarda solo el puntero, no los datos completos.
graph TD
Input["Set Key: Value"] --> Check{"Es Objeto?"}
Check -- No --> SaveDirect[Guardar valor en Index actual]
Check -- Sí --> ShardForge[Llamar a Shard.forge]
ShardForge --> GenID[Generar ID Único]
ShardForge --> WriteFile[Escribir Value en ID.bin]
ShardForge --> ReturnRef["Retornar { $file: ID.bin }"]
ReturnRef --> SaveDirect
SaveDirect --> CheckThres{"Contador >= Threshold?"}
CheckThres -- Sí --> DiskWrite[Escribir Index en Disco ahora]
CheckThres -- No --> SetTimer[Iniciar Timer Debounce]
SetTimer -->|Timeout| DiskWrite
HyperDB utiliza Proxy de ES6 para interceptar todas las operaciones. La clase Flow y el mecanismo de DB.js gestionan la "navegación" por la base de datos.
- Navegación Lazy: Cuando accedes a
db.data.usuario.perfil, el sistema no cargaperfilhasta que lo tocas. - Apertura Dinámica: Si el valor recuperado es un puntero
{ $file: "..." }, el métodoProxyenDB.jsdetecta esto, lee el archivo desdeDisk(oMemory), crea un nuevoClusterpara ese archivo y devuelve un nuevoProxyenvolviendo esos datos. - Flow Tree: La clase
Flowmantiene un árbol paralelo de referencias. Esto permite inyectar lógica personalizada o metadatos en rutas específicas del árbol de datos sin contaminar los datos almacenados.
HyperDB permite reemplazar casi cualquiera de sus componentes internos pasando clases o instancias en el constructor. Esto es útil para testing (mocks) o para cambiar el comportamiento (ej. guardar en S3 en lugar de disco local).
Si quieres usar HyperDB puramente en memoria (sin escribir a disco) para pruebas unitarias:
import { HyperDB, Disk } from 'hyper-db';
// Creamos un Disk personalizado o configurado solo en memoria
// Nota: La clase Disk original ya soporta esto si no se le da folder,
// pero aquí forzamos el comportamiento mediante inyección.
class InMemoryDisk extends Disk {
write(filename, data) {
// Sobrescribir para no tocar 'fs'
this.memory.set(filename, data);
return Promise.resolve(true);
}
// ... implementar resto de métodos necesarios
}
const db = new HyperDB({
$class: {
Disk: new InMemoryDisk()
}
});El código en DB.js verifica DB.shared. Esto permite añadir métodos globales disponibles en cualquier nivel del proxy.
// (Internamente en una versión modificada de HyperDB)
this.shared = {
hola: function() { console.log("Hola desde", this.index.$file); }
}
// Uso:
// db.data.users.hola() -> Imprime el archivo donde viven los usuariosLa clase Disk acepta un callback onError. Por defecto, imprime a console.error.
const db = new HyperDB({
$class: {
Disk: new Disk({
onError: (err) => {
// Enviar a sistema de logs (Sentry, Datadog, etc.)
alertAdmin("Error crítico de IO en DB", err);
}
})
}
});Con el tiempo, al borrar objetos, pueden quedar carpetas vacías en la estructura de shards. El método prune las elimina.
// Se recomienda ejecutar esto en tareas cron o al inicio/cierre
await db.disk.prune();Si la aplicación se cierra inesperadamente (SIGKILL), los archivos .tmp pueden quedar en la carpeta de datos.
- Al inicio: HyperDB no limpia automáticamente los
.tmp. - Integridad: Dado que la operación de renombrado (
fs.rename) es atómica en sistemas POSIX, el archivo.binoriginal estará intacto o será la nueva versión completa. No hay estados intermedios de archivo corrupto.
package.json: Metadatos y scripts (sin tests definidos).Disk.js: Capa física.fs,v8,Pipe,Memory.Memory.js: Capa lógica de caché. LRU, cálculo de tamaño.Shard.js: Lógica de particionado. Generación de IDs, purga recursiva.Flow.js: Utilidad de estructura de árbol para metadatos de navegación.DB.js: Entry point. Gestión de Proxies e Inyección de Dependencias.Cluster.js: Gestión de nodos.Index,File(debounce/threshold),Cluster(lógica get/set).index.js: Exportador de módulos.
Aquí tienes la tercera y última parte de la documentación técnica de HyperDB. Esta sección cubre Seguridad, Optimización de Rendimiento, Solución de Problemas y una Referencia Rápida de API.
Dado que HyperDB opera directamente sobre el sistema de archivos local y utiliza serialización binaria, existen vectores de seguridad específicos que deben considerarse.
HyperDB utiliza el motor nativo de V8 para serializar/deserializar datos.
- Riesgo: La deserialización de datos no confiables puede llevar a la ejecución de código arbitrario o comportamientos inestables si el archivo binario ha sido manipulado externamente.
- Mitigación: Asegúrese de que la carpeta de datos (
./datapor defecto) tenga permisos de escritura exclusivos para el usuario del sistema que ejecuta el proceso Node.js. Nunca apunte HyperDB a una carpeta donde usuarios externos puedan subir archivos.
HyperDB necesita permisos de lectura y escritura (fs.read, fs.write, fs.mkdir, fs.rm).
- Requisito: El proceso Node.js debe tener control total sobre el directorio raíz definido en
options.folder. - Error Común: Ejecutar la aplicación como
rooty luego comouserpuede causar errores deEACCESsi los archivos fueron creados porroot.
La característica de inyección ($class) permite reemplazar componentes internos.
- Precaución: Si su aplicación permite configuración externa que se pasa directamente al constructor de
HyperDB, un atacante podría inyectar clases maliciosas. Valide siempre el objetooptionsantes de pasarlo al constructor.
Para obtener el máximo rendimiento (High-Performance) prometido por HyperDB, siga estas directrices:
El sharding automático es potente, pero tiene un costo de I/O (crear archivo, abrir handle, escribir).
- Cuándo usarlo: Para objetos grandes o listas que crecen indefinidamente (ej.
db.data.usuarios,db.data.logs). - Cuándo evitarlo: Para objetos pequeños o de configuración (ej.
{ x: 1, y: 2 }). Si un objeto tiene pocas propiedades primitivas, es mejor dejarlo dentro del archivo padre (Index) en lugar de aislarlo en un nuevo archivo. - Consejo: HyperDB decide hacer shard si asignas un
Plain Object. Si asignas primitivos (string,number), se quedan en el archivo actual.
- Escenario: Servidor con mucha RAM.
- Acción: Aumente
options.memory. Por defecto es 20MB. Subirlo a 100MB o 500MB reducirá drásticamente las lecturas a disco, ya que más shards permanecerán "calientes" en RAM.
- Acción: Aumente
- Escenario: Entorno Serverless (AWS Lambda) o contenedores pequeños.
- Acción: Mantenga el límite bajo (10-20MB) y reduzca
options.index.debouncea valores cercanos a 0 o 100ms para asegurar que los datos se escriban antes de que la función se congele.
- Acción: Mantenga el límite bajo (10-20MB) y reduzca
Las operaciones de escritura son asíncronas y diferidas ("debounced").
- Mejor Práctica: Siempre llame a
await db.flush()antes de finalizar el proceso o en puntos críticos de la lógica de negocio donde la persistencia inmediata es obligatoria.
- Causa: La aplicación se detuvo forzosamente (Crash o
SIGKILL) antes de que el temporizador de debounce (por defecto 3-5 segundos) se disparara. - Solución: Reduzca los tiempos de
debounceen la configuración o asegúrese de capturar eventos de cierre (process.on('SIGINT', ...)) y llamar adb.flush().
- Causa: Se pasó
null,undefinedo un tipo no objeto al constructor. - Verificación: Asegúrese de instanciar con
{}como mínimo:new HyperDB({}).
- Causa:
depth: 0con miles de objetos crea miles de archivos en una sola carpeta. - Solución: Use
depth: 2o superior en la configuración. Esto distribuye los archivos en subcarpetas (ej.A1/B2/...), lo cual es más amigable para sistemas de archivos como EXT4 o NTFS.
- Síntoma: Advertencia en consola
[HyperDB:Memory] Object '...' exceeds cache limit. - Causa: Está intentando guardar un solo objeto (un solo shard) que es más grande que el límite total de memoria asignado a la DB.
- Solución: Aumente
options.memoryo divida ese objeto gigante en sub-objetos más pequeños para que HyperDB pueda fragmentarlo.
Resumen de los métodos y propiedades expuestos para el desarrollador.
const db = new HyperDB(options);Ver sección de configuración para detalles de options.
db.data: (Proxy) El punto de entrada principal para leer y escribir datos. Se comporta como un objeto JS estándar.db.disk: (Instancia deDisk) Acceso directo a operaciones de archivo (bajo nivel).db.index: (Instancia deIndex) Acceso al gestor del archivo índice raíz.
db.open(...path):- Descripción: Abre manualmente una ruta específica y devuelve un Proxy para ese nodo.
- Uso:
const userConfig = db.open('users', 'id_123', 'config');
db.flush():- Descripción: Fuerza la escritura de todas las operaciones pendientes en la cola (
Pipe) y espera a que terminen. - Retorno:
Promise<true>
- Descripción: Fuerza la escritura de todas las operaciones pendientes en la cola (
db.memory():- Descripción: Devuelve estadísticas del uso de memoria actual.
- Retorno:
{ used: string, limit: string, items: number }
delete db.data.propiedad:- Descripción: Elimina la clave y, eventualmente, purga el archivo asociado del disco.
db.disk.prune():- Async. Escanea y elimina carpetas vacías en el directorio de datos.
Para entender dónde encaja HyperDB:
| Característica | HyperDB | JSON (fs.writeFile) | SQLite | MongoDB |
|---|---|---|---|---|
| Formato | Binario (V8) Fragmentado | Texto Plano (Monolítico) | Binario (Relacional) | Binario (BSON) |
| Carga en RAM | Lazy (Solo lo necesario) | Todo el archivo | Paginada | Paginada |
| Escritura | Atómica y Parcial | Reescribe todo el archivo | Transaccional (SQL) | Documento |
| Dependencias | Cero (Nativo) | Cero | sqlite3 (binding C++) |
Driver + Servidor |
| Consultas | Traversal (JS nativo) | Array methods | SQL | Query Language |
| Uso Ideal | Config, Estado local, Grafos de objetos | Config simple | Datos tabulares | Big Data / Cloud |
Conclusión: HyperDB es ideal cuando JSON se queda corto por rendimiento/memoria, pero SQLite es excesivo o demasiado rígido (schemas) para la estructura de datos dinámica que maneja la aplicación.