simple-key-value
v1.0.0
Published
Librería estilo Redis (get/set con TTL) para Node.js, escrita en TypeScript, con adapters de persistencia intercambiables (memoria, filesystem, sqlite, mysql, redis). ESM, compatible con Next.js.
Maintainers
Readme
simple-key-value
Librería estilo Redis para Node.js, escrita en TypeScript: API get/set
con soporte de TTL, tipos completos (.d.ts incluido) y persistencia
intercambiable mediante adapters (memoria, filesystem, SQLite, MySQL,
Redis, o los que quieras agregar).
Paquete ESM puro ("type": "module"). Pensado para correr en un
servidor Node de larga duración (VPS, Docker, PM2, etc.) — ver la
sección de Next.js más abajo si es tu caso.
Instalación
npm install ./simple-key-value # o copiá la carpeta a tu proyecto
npm run build # compila src/ (TS) -> dist/ (JS + .d.ts)Sin dependencias obligatorias en runtime. Los adapters de SQLite/MySQL/Redis
son opcionales: solo hace falta instalar el driver correspondiente si los
usás (se cargan con import() dinámico, así que no rompen nada si no están).
Uso básico
import { Store } from 'simple-key-value';
const store = new Store(); // por defecto: en memoria, sin persistencia real
await store.init();
interface Sesion {
token: string;
}
await store.set('user:1', { name: 'Ana' });
await store.set<Sesion>('session:abc', { token: 'xyz' }, { ttl: 60_000 }); // expira en 60s
const sesion = await store.get<Sesion>('session:abc'); // tipado: Sesion | undefined
await store.has('user:1'); // -> boolean
await store.ttl('session:abc'); // -> ms restantes
await store.expire('user:1', 5000); // le pone TTL a una clave que no tenía
await store.persist('user:1'); // le saca el TTL
await store.del('user:1');
await store.keys(); // -> string[] de claves vigentes
await store.clear();
await store.close(); // persiste lo pendiente y libera recursosget<T>() y set<T>() son genéricos: pasá el tipo esperado y TypeScript
infiere el resultado por vos. Si no lo pasás, cae a unknown.
Uso en un VPS (long-running process)
Este es el escenario ideal para la librería: el proceso Node vive indefinidamente, así que:
- El sweep de TTL en background (
sweepIntervalMs) corre normalmente y limpia claves vencidas de forma proactiva. - Podés usar
FileAdapteroSQLiteAdaptersin problema, porque el filesystem es persistente y no hay cold starts que lo compliquen. - Conviene instanciar el
Storeuna sola vez al arrancar la app (ej. en tuserver.tso en un módulolib/kv.ts) y reutilizarlo en toda la aplicación, en vez de crear uno nuevo por request.
// lib/kv.ts
import { Store, adapters } from 'simple-key-value';
import path from 'node:path';
export const store = new Store({
adapter: new adapters.SQLiteAdapter({ file: path.join(process.cwd(), 'data', 'kv.sqlite') }),
});
await store.init();
process.on('SIGTERM', async () => {
await store.close();
process.exit(0);
});Uso en Next.js
Los adapters (salvo MemoryAdapter) usan APIs de Node (fs,
better-sqlite3, mysql2, ioredis), así que en Next.js solo pueden
correr en el servidor (Route Handlers, Server Actions, Server Components),
nunca en Client Components ni en Edge Runtime.
Igual que en el VPS: usá un singleton para no perder el estado en memoria ni reabrir el adapter en cada invocación.
// lib/kv.ts
import { Store, adapters } from 'simple-key-value';
import path from 'node:path';
let storePromise: Promise<Store> | undefined;
export function getStore(): Promise<Store> {
if (!storePromise) {
const store = new Store({
adapter: new adapters.FileAdapter({
filePath: path.join(process.cwd(), '.data', 'kv.json'),
}),
});
storePromise = store.init().then(() => store);
}
return storePromise;
}// app/api/kv/route.ts
import { getStore } from '@/lib/kv';
export const runtime = 'nodejs'; // importante: no 'edge'
export async function GET(request: Request) {
const store = await getStore();
const { searchParams } = new URL(request.url);
const key = searchParams.get('key');
const value = key ? await store.get(key) : undefined;
return Response.json({ value });
}Si desplegás en serverless (Vercel, Lambda) en vez de un VPS, el
filesystem no persiste de forma confiable entre invocaciones: ahí conviene
RedisAdapter (por ejemplo con Upstash) o MySQLAdapter en lugar de
FileAdapter/SQLiteAdapter.
API
| Método | Descripción |
|---|---|
| store.init(): Promise<this> | Carga datos del adapter y arranca el sweep de TTL. Llamar antes de usar el store. |
| store.set<T>(key: string, value: T, opts?: { ttl?: number }): Promise<this> | Guarda un valor. ttl en milisegundos (opcional). |
| store.get<T>(key: string): Promise<T \| undefined> | Devuelve el valor tipado, o undefined si no existe/expiró. |
| store.has(key: string): Promise<boolean> | — |
| store.del(key: string): Promise<boolean> | Devuelve true si existía. |
| store.ttl(key: string): Promise<number> | -2 no existe, -1 sin TTL, o ms restantes. |
| store.expire(key: string, ttlMs: number): Promise<boolean> | Actualiza el TTL de una clave existente. |
| store.persist(key: string): Promise<boolean> | Quita el TTL (pasa a persistir indefinidamente). |
| store.keys(): Promise<string[]> | Claves vigentes. |
| store.clear(): Promise<void> | Borra todo. |
| store.flush(): Promise<void> | Fuerza guardado inmediato en el adapter. |
| store.close(): Promise<void> | Guarda pendientes, limpia timers y cierra el adapter. |
Eventos (tipados)
Store extiende EventEmitter, con on/once/off/emit tipados
contra StoreEvents:
store.on('expired', (key) => console.log('expiró:', key)); // key: string
store.on('set', (key, value) => { ... }); // key: string, value: unknown
store.on('del', (key) => { ... });
store.on('error', (err) => { ... });
store.on('ready', () => { ... });Adapters de persistencia
import { Store, adapters } from 'simple-key-value';MemoryAdapter (default)
Sin persistencia real, solo vive en RAM. Útil para tests o caché puro.
FileAdapter
Persiste como JSON en disco, con escritura atómica.
const store = new Store({
adapter: new adapters.FileAdapter({ filePath: './data.json' }),
});SQLiteAdapter (requiere npm install better-sqlite3)
const store = new Store({
adapter: new adapters.SQLiteAdapter({ file: './data.sqlite' }),
});MySQLAdapter (requiere npm install mysql2)
const store = new Store({
adapter: new adapters.MySQLAdapter({
connection: { host: 'localhost', user: 'root', password: '...', database: 'app' },
}),
});RedisAdapter (requiere npm install ioredis)
const store = new Store({
adapter: new adapters.RedisAdapter({ redisOptions: { host: 'localhost' } }),
});Los tipos de estos tres adapters usan declaraciones ambientales (
any) para no forzar sus paquetes como dependencia obligatoria. Si instalás el paquete real, sus propios tipos siguen disponibles para vos al usarlo directamente; internamente el adapter sigue tipado comoanypor diseño.
Crear tu propio adapter
Extendé BaseAdapter e implementá load() y save(snapshot)
(y opcionalmente init()/close()):
import { BaseAdapter, type Snapshot } from 'simple-key-value';
export class MiAdapter extends BaseAdapter {
async load(): Promise<Snapshot> { /* devolver snapshot */ }
async save(snapshot: Snapshot): Promise<void> { /* persistir snapshot */ }
}Notas de diseño
- ESM + TS: el paquete es
"type": "module", escrito en TypeScript y compilado contscadist/(JS +.d.ts). Si tu proyecto es CommonJS, importalo conconst { Store } = await import('simple-key-value');(import dinámico), ya que un paquete ESM no se puederequire(). - TTL: cada entrada guarda
expiresAt(timestamp absoluto). Un sweep en background (sweepIntervalMs, default 1000ms) limpia claves vencidas, y ademásget/hasverifican expiración al vuelo — por eso el TTL es correcto incluso si el proceso se reinicia entre chequeos. - Persistencia: los adapters trabajan con un "snapshot" completo del
store. Tras cada mutación se agenda un guardado con debounce
(
saveDebounceMs, default 100ms). Los adapters de SQLite/MySQL incluidos reescriben la tabla completa en cadasave()por simplicidad — para datasets grandes en producción conviene evolucionar eso a escrituras incrementales por clave. - Al cargar (
init()), las claves que ya expiraron mientras el proceso estaba apagado no se restauran.
Scripts
npm run build # compila src/ (TS) -> dist/ (JS + .d.ts)
npm run typecheck # solo chequea tipos, sin emitir archivos
npm test # corre los tests (vía tsx, sin necesidad de build previo)
npm run test:dist # compila y corre los tests contra dist/
npm run example # corre example.ts