v0.1.0

Roadmap

Plan de desarrollo de Velo: que esta listo y que viene despues.


Vision del proyecto

Velo nace como un ejercicio educativo de bajo nivel: construir desde cero, sin dependencias, un almacen de datos en memoria accesible por red con el protocolo RESP. El objetivo no es reemplazar soluciones de produccion, sino entender exactamente como funcionan por dentro: TCP, parsing binario, Clean Architecture, TTL, pub/sub, y eventual distribucion.

ℹ️
Cada fase agrega un grupo de funcionalidades coherente, sin romper lo anterior. La arquitectura Clean hace posible agregar nuevos comandos y capas de infraestructura sin tocar el dominio ni los casos de uso existentes.

Fases de desarrollo

Diagrama de roadmap con las 6 fases de Velo
Diagrama β€” Roadmap de Velo: Fase 1 completada, Fases 2-6 planificadas

Fase 1 β€” Strings y Servidor Completada v0.1.0

La primera fase establece la base tecnica completa: protocolo, arquitectura, transporte y las operaciones mas fundamentales de cualquier almacen clave-valor.

Infraestructura TCP
Servidor TCP con net.createServer, limite configurable de conexiones, shutdown graceful con limpieza de sockets activos.
Parser RESP
Parser incremental de Buffers que maneja fragmentacion de paquetes TCP y pipelining (multiples comandos en un chunk). Soporte inline para netcat/telnet.
15 Comandos String
SET con modificadores (NX, XX, EX, PX), GET, DEL, EXISTS, EXPIRE, TTL, INCR/DECR, MGET/MSET, APPEND, STRLEN, SETNX, SETEX, GETEX.
8 Comandos Servidor
PING, ECHO, DBSIZE, FLUSHALL, KEYS (con glob), TYPE, RENAME, SELECT. 16 bases de datos aisladas por conexion.
TTL Dual
Lazy expiry O(1) en cada acceso + sweep periodico configurable que limpia claves huerfanas sin bloquear el event loop.
REPL de cliente
Cliente interactivo con historico de comandos, prompt dinamico (muestra la DB activa), salida coloreada por tipo RESP y sistema de cola para pipelining.

Fase 2 β€” Listas Pendiente

Las listas son la estructura de datos mas versatil: colas, stacks, logs de actividad, historial de eventos. Se implementaran como arrays de JavaScript vinculados a una KeyEntry con type: TipoDato.LIST.

ComandoDescripcion
LPUSH key val [val...]Inserta uno o varios valores al inicio de la lista
RPUSH key val [val...]Inserta uno o varios valores al final de la lista
LPOP key [count]Extrae y devuelve elementos desde el inicio
RPOP key [count]Extrae y devuelve elementos desde el final
LRANGE key start stopDevuelve un rango de elementos (indices negativos desde el final)
LLEN keyDevuelve la longitud de la lista
LINDEX key indexDevuelve el elemento en la posicion indicada
LSET key index valReemplaza el elemento en la posicion indicada
LREM key count valElimina hasta count ocurrencias del valor

Fase 3 β€” Hashes y Sets Pendiente

Hashes

Objetos de campo/valor almacenados como Map<string, string>. Utiles para representar entidades como usuarios o sesiones.

ComandoDescripcion
HSET key field value [field value...]Establece uno o varios campos
HGET key fieldDevuelve el valor de un campo
HDEL key field [field...]Elimina campos
HGETALL keyDevuelve todos los campos y valores
HKEYS / HVALS keyDevuelve solo los campos o solo los valores
HMSET / HMGETOperaciones masivas (multiples campos)

Sets

Colecciones de strings unicos sin orden. Implementados como Set<string> de JavaScript.

ComandoDescripcion
SADD key member [member...]Agrega miembros al set
SREM key member [member...]Elimina miembros
SMEMBERS keyDevuelve todos los miembros
SCARD keyDevuelve el numero de miembros
SISMEMBER key memberDevuelve 1 si el miembro existe
SINTER / SUNION / SDIFFInterseccion, union y diferencia de sets

Fase 4 β€” Persistencia Pendiente

Sin persistencia, todos los datos mueren con el proceso. Hay dos estrategias clasicas:

Snapshots (RDB)
El store serializa su contenido completo a disco en intervalos configurables (o manualmente con BGSAVE). Rapido de cargar, puede perder datos de la ultima ventana de tiempo entre snapshots.
Append-Only File (AOF)
Cada escritura se registra en un archivo de log. Al reiniciar, el server reproduce el log para reconstruir el estado. Mas durabilidad, archivos mas grandes. Se podra compactar con BGREWRITEAOF.
✦
Ambas estrategias se implementaran con fs.createWriteStream y fs.createReadStream nativos. Sin dependencias externas.

Fase 5 β€” Pub/Sub Pendiente

El sistema de publicacion y suscripcion permite que los clientes reciban mensajes en tiempo real sin hacer polling. Ideal para notificaciones, colas de eventos y comunicacion entre servicios.

ComandoDescripcion
SUBSCRIBE canal [canal...]Suscribe al cliente a uno o varios canales
UNSUBSCRIBE [canal...]Cancela la suscripcion (todos los canales si no se especifica)
PUBLISH canal mensajeEnvia un mensaje a todos los suscriptores del canal
PUBSUB CHANNELS [patron]Lista los canales activos (con suscriptores)
PUBSUB NUMSUB [canal...]Devuelve el numero de suscriptores por canal

La implementacion usara un PubSubBroker como singleton que mantiene un Map<canal, Set<socket>>. Cuando llega un PUBLISH, el broker itera el Set y escribe a cada socket activo.

Fase 6 β€” Cluster Pendiente

La fase final introduce distribucion real: multiples nodos, tolerancia a fallos y escalabilidad horizontal.

Replicacion
Un nodo primario acepta escrituras y las replica a N nodos replica. Las replicas pueden servir lecturas. Failover automatico si el primario cae.
Sharding por hash slot
Las 16384 hash slots se distribuyen entre los nodos. El cliente recibe redirecciones MOVED y ASK cuando una clave vive en otro nodo.
Gossip protocol
Los nodos se comunican entre si periodicamente para detectar fallos, propagar el estado del cluster y elegir nuevos primarios.

Como agregar un nuevo comando

Agregar un comando en Velo es un proceso de 3 pasos. La arquitectura asegura que cada paso sea aislado y no afecte al resto del sistema.

1
Crear el handler del comando
Crea una clase que implemente el metodo execute(store, args, db). El archivo va en src/application/usecases/phaseX/categoria/NombreCommand.js.
javascript β€” src/application/usecases/phase2/lists/LpushCommand.js
import { RespSerializer  } from '../../../infrastructure/protocol/RespSerializer.js';
import { KeyEntry, TipoDato } from '../../../domain/entities/KeyEntry.js';

export class LpushCommand {
  /** @type {string} */
  nombre = 'LPUSH';

  /**
   * Inserta valores al inicio de una lista.
   * @param {import('../../../domain/ports/IDataStore.js').IDataStore} store
   * @param {string[]} args
   * @param {number} db
   */
  execute(store, args, db) {
    if (args.length < 2) {
      return RespSerializer.error('ERR wrong number of arguments for LPUSH');
    }

    const [clave, ...valores] = args;
    let entrada = store.get(db, clave);

    if (!entrada) {
      entrada = new KeyEntry([], TipoDato.LIST);
    } else if (entrada.type !== TipoDato.LIST) {
      return RespSerializer.error('WRONGTYPE Operation against a key holding the wrong kind of value');
    }

    // LPUSH inserta al inicio (unshift)
    entrada.value.unshift(...valores.reverse());
    store.set(db, clave, entrada);

    return RespSerializer.integer(entrada.value.length);
  }
}
2
Registrar el comando
Abre src/application/CommandRegistry.js e importa y registra la nueva clase.
javascript β€” src/application/CommandRegistry.js (fragmento)
import { LpushCommand } from './usecases/phase2/lists/LpushCommand.js';

export class CommandRegistry {
  constructor() {
    this._handlers = new Map();
    this._registrar();
  }

  _registrar() {
    // ... comandos existentes ...

    // Fase 2 β€” Listas
    const listaComandos = [new LpushCommand()];
    for (const cmd of listaComandos) {
      this._handlers.set(cmd.nombre.toUpperCase(), cmd);
    }
  }
}
3
Probar el comando
Arranca el servidor con make server y el cliente con make client. El nuevo comando ya esta disponible de inmediato.
bash β€” verificacion en el REPL
velo[0]> LPUSH tareas "leer docs" "escribir tests" "hacer PR"
:3
velo[0]> LRANGE tareas 0 -1
1) "hacer PR"
2) "escribir tests"
3) "leer docs"
✦
No se necesita reiniciar el servidor entre pruebas durante el desarrollo. Usa make dev que arranca con la bandera --watch de Node.js para recarga automatica.

Convenciones del proyecto

ReglaDescripcion
Sin dependencias Solo modulos nativos de Node.js. No se permite ningun paquete en dependencies.
ESM puro Todos los archivos usan import/export. No CommonJS (require).
Comentarios en espanol Todos los comentarios de codigo y logs se escriben en espanol sin tildes ni caracteres especiales.
Un comando = un archivo Cada comando es su propia clase en su propio archivo. Nunca se agrupan multiples comandos.
Sin logica en infraestructura Los archivos de infrastructure/ no contienen logica de negocio. Solo transporte y serializaciΓ³n.
Errores RESP siempre Cualquier error en un comando se devuelve como RespSerializer.error('ERR ...'). Nunca se lanza una excepcion no capturada al cliente.
Node.js >= 20 Se requiere Node 20+ para ESM estable. Se recomienda Node 22 para la sintaxis import ... with { type: 'json' }.