npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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 recursos

get<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 FileAdapter o SQLiteAdapter sin problema, porque el filesystem es persistente y no hay cold starts que lo compliquen.
  • Conviene instanciar el Store una sola vez al arrancar la app (ej. en tu server.ts o en un módulo lib/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 como any por 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 con tsc a dist/ (JS + .d.ts). Si tu proyecto es CommonJS, importalo con const { Store } = await import('simple-key-value'); (import dinámico), ya que un paquete ESM no se puede require().
  • TTL: cada entrada guarda expiresAt (timestamp absoluto). Un sweep en background (sweepIntervalMs, default 1000ms) limpia claves vencidas, y además get/has verifican 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 cada save() 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