Ir al contenido

KV Kit

Store de key-value con soporte para Redis y memoria.

import { App } from '@iskra-bun/core';
import { KVManager } from '@iskra-bun/kv-kit';
const app = new App({
name: 'MiApp',
kv: {
driver: 'redis',
connection: { url: 'redis://localhost:6379' },
},
});
const kv = new KVManager();
app.register(kv);
await app.start();
// Usar el store
await kv.set('usuario:123', { name: 'Juan', role: 'admin' });
const user = await kv.get('usuario:123');
await kv.del('usuario:123');

Si no configuras nada, usa el adapter de memoria:

const app = new App({ name: 'MiApp' });
const kv = new KVManager(); // usa memoria por defecto

connection se pasa directamente al cliente ioredis, por lo que acepta tanto un objeto de opciones (RedisOptions) como una cadena de conexión:

const app = new App({
name: 'MiApp',
kv: {
driver: 'redis',
connection: { url: process.env.REDIS_URL || 'redis://localhost:6379' },
},
});
// O una cadena de conexión:
const app = new App({
name: 'MiApp',
kv: { driver: 'redis', connection: 'redis://localhost:6379' },
});

app.start() espera a que Redis responda, así que un servidor inalcanzable o una contraseña incorrecta hacen fallar el arranque en vez del primer comando. Una vez conectado, ioredis reintenta si se cae la conexión, y sus errores de conexión van al logger de la app (warn).

// Guardar (con TTL opcional en segundos)
await kv.set('clave', valor);
await kv.set('clave', valor, 60); // expira en 60 segundos
// Obtener — devuelve undefined si la clave no existe
const data = await kv.get('clave');
// Eliminar
await kv.del('clave');
// Verificar existencia
const existe = await kv.has('clave');
// Eliminar todas las claves del namespace (ver "Borrar claves")
await kv.clear();

Con el driver redis, kv.client es el cliente de ioredis (despues de app.start()) para comandos que la API KV no cubre (sets, sorted sets, pipelines). No aplica el namespace ni el codec JSON. Es undefined con el driver memory. El KVManager tambien queda registrado en el contexto de la app:

const kv = app.context.get('kv'); // el KVManager
await kv.client?.sadd('tags', 'a', 'b');

get y set aceptan un parámetro de tipo para obtener lecturas tipadas en lugar de any:

interface Usuario {
nombre: string;
rol: string;
}
// Escritura tipada
await kv.set<Usuario>('usuario:123', { nombre: 'Juan', rol: 'admin' });
// Lectura tipada — resuelve a Usuario | undefined
const usuario = await kv.get<Usuario>('usuario:123');
if (usuario) {
console.log(usuario.rol); // string, no any
}

Una clave inexistente resuelve a undefined (no null). Anteriormente el adaptador Redis podía devolver null para claves inexistentes; ahora está normalizado en todos los adaptadores.

El adaptador de Redis usa un único codec consistente para cada escritura y lectura: los valores se serializan con JSON.stringify al escribir y se parsean con JSON.parse al leer. Strings, números, booleanos, y objetos y arrays planos de ellos conservan su tipo:

await kv.set('numerica', '123'); // string
typeof (await kv.get('numerica')); // 'string' — NO se convierte en número 123
await kv.set('jsonish', '{}'); // string
await kv.get('jsonish'); // '{}' — sigue siendo string, NO un objeto vacío
await kv.set('contador', 42); // number
typeof (await kv.get('contador')); // 'number'

undefined se trata de forma explícita (se almacena como el literal JSON null y se decodifica de vuelta a undefined), por lo que nunca se corrompe en la cadena "undefined". Valores escritos fuera del adaptador que no sean JSON válido se devuelven tal cual (como cadena).

Lo que JSON no puede representar no vuelve igual, a diferencia del adaptador de memoria, que guarda una copia del valor mismo: un Date vuelve como string ISO, un Map o Set como {}, undefined dentro de un array como null, y null como undefined. Esos valores hay que convertirlos a mano (date.toISOString() / new Date(s), Object.fromEntries(map)).

Pasa la opción namespace para prefixar automáticamente cada clave y evitar colisiones entre módulos que comparten el mismo store:

const sesiones = new KVManager({ namespace: 'sessions' });
const cache = new KVManager({ namespace: 'cache' });
// Escriben en "sessions:token" y "cache:token" — sin colisión
await sesiones.set('token', datosSesion);
await cache.set('token', respuestaCache);

El prefijo se aplica de forma transparente; nunca lo incluyes en tus cadenas de clave. La opción tiene como valor predeterminado "" (sin prefijo) para que el código existente no se vea afectado.

kv.clear(prefix?) elimina todas las claves del namespace del manager (solo las que están bajo prefix dentro de él, si se indica), sin tocar la conexión:

  • Memoria: se eliminan las claves que coinciden.
  • Redis: las claves se buscan con SCAN y se eliminan con DEL, limitadas al namespace (y al keyPrefix de ioredis, si configuraste uno). Sin namespace, clear() vaciaría toda la base de Redis, que pueden compartir otras apps o servicios, así que lanza un error salvo que lo habilites con flushDb: true, que ejecuta FLUSHDB:
const cache = new KVManager({ namespace: 'cache' });
await cache.clear(); // elimina solo cache:*
const propia = new KVManager({ flushDb: true }); // la base es solo de esta app
await propia.clear(); // FLUSHDB

El clear() de cache-kit usa este clear(), así que una Cache sobre un KVManager borra solo sus propias claves.

sadd(key, member, ttl?) y sdrain(key) mantienen un conjunto de strings cuyos miembros expiran uno por uno: el conjunto vive lo que su miembro más duradero, y sdrain lo elimina y devuelve sus miembros no expirados en un solo paso (un miembro expirado que aún siga guardado se omite). En Redis es un sorted set ordenado por expiración, actualizado y vaciado por scripts atómicos. cache-kit guarda en ellos su índice de etiquetas. Solo key lleva el namespace; los miembros se guardan tal cual.

KVManager expone tres helpers de lote que ejecutan sus llamadas subyacentes de forma concurrente:

// Leer varias claves a la vez — preserva el orden, undefined para claves inexistentes
const [a, b, c] = await kv.mget<string>(['clave:a', 'clave:b', 'clave:c']);
// Escribir varios pares clave/valor (TTL compartido opcional)
await kv.mset({ 'clave:a': 'alfa', 'clave:b': 'beta' });
await kv.mset([['clave:c', 'gamma'], ['clave:d', 'delta']], 120); // TTL = 120 s
// Eliminar varias claves
await kv.mdel(['clave:a', 'clave:b', 'clave:c']);

mset acepta tanto un array de tuplas [clave, valor] como un objeto plano.

Un TTL es una cantidad de segundos; 0 (o ninguno) significa sin expiración, y un TTL negativo o no finito se rechaza con un RangeError.

El adaptador en memoria gestiona los temporizadores de expiración sin fugas: sobrescribir una clave con una nueva llamada a set cancela cualquier temporizador previo antes de programar uno nuevo, por lo que un temporizador obsoleto nunca puede eliminar un valor recién escrito. Se admiten TTLs más largos que el límite de setTimeout (unos 24,8 días).

Como Redis, el adaptador en memoria guarda y devuelve copias (structuredClone): modificar un objeto que pasaste a set, o uno que devolvió get, no cambia el valor guardado (antes era el mismo objeto, así que el cambio de una petición aparecía en todas las demás). Un valor que no se puede clonar, como una función, se rechaza; una instancia de clase vuelve como objeto plano.

Ventana de terminal
REDIS_URL=redis://localhost:6379