Skip to content

PerryTS/redis

Repository files navigation

@perryts/redis

A pure-TypeScript Redis / Valkey driver that speaks the wire protocol directly — no native addons, no hiredis, no FFI shims. It runs unchanged on Node.js, Bun, and Perry, where the same source is ahead-of-time compiled to a native binary via LLVM — giving you a real standalone executable that talks to Redis with no JS runtime attached.

Perry is a TypeScript-to-native compiler: it lowers a strict subset of TS through LLVM into a statically-linked binary. @perryts/redis is the third reference driver in the family (after @perryts/postgres and @perryts/mysql), showcasing Perry's ability to host real systems code: every socket read, TLS handshake, and crypto op goes through perry-stdlib.

bun add @perryts/redis
npm install @perryts/redis
pnpm add @perryts/redis
import { connect } from '@perryts/redis';

const conn = await connect({ host: '127.0.0.1', port: 6379 });

await conn.command('SET', 'greeting', 'hello from perry');
const got = await conn.command<Buffer>('GET', 'greeting');
console.log(got.value?.toString('utf8'));   // → 'hello from perry'

await conn.close();

Why another Redis driver?

Most Node Redis clients (ioredis, node-redis) work great on V8 but make assumptions a strict TypeScript-to-native compiler can't honor: runtime-mutable prototypes, dynamic method dispatch, process.nextTick, EventEmitter inheritance, require('hiredis') for parsing speed. None of those translate cleanly to a Perry binary.

@perryts/redis was designed against the Perry AOT constraint set from day one: module-level state maps instead of this.method closures, explicit branching instead of ?. / ??, no for-of on arrays, no dynamic key access. The same source compiles to a native binary AND runs unchanged on Node.js / Bun.

Features

  • RESP2 + RESP3 — both parsers ship from day one. HELLO 3 is tried by default; on -NOPROTO we fall back to RESP2 + legacy AUTH transparently.
  • Authentication — legacy requirepass, ACL (HELLO 3 AUTH user pass), pluggable via registerAuthPlugin.
  • TLS — full TLS-from-SYN via rediss:// (no mid-stream upgrade).
  • Pipeliningclient.pipeline().add(...).add(...).exec() writes N commands in one syscall; per-command errors don't fail the batch.
  • Pub/Subclient.subscribe(chan, cb) works against both RESP2 array messages and RESP3 push frames; the callback shape is uniform.
  • Transactionsclient.multi() builder; WATCH-abort returns { aborted: true } cleanly.
  • Scriptsclient.eval / client.evalsha with automatic -NOSCRIPT retry via cached source.
  • Client-side cachingclient.tracking.enable({...}) + tracking.on('invalidate', cb) for RESP3 invalidation pushes.
  • Blocking commandsclient.blockingCommand(args, timeoutMs) sets a pool-visible busy flag so BLPOP / WAIT / XREAD BLOCK don't break pooling.
  • Cluster — CRC16 slot routing, MOVED/ASK redirection, per-node pools, hashtag co-location.
  • Pool — idle set + waiting queue + RESET-on-release.
  • Cancel — second-connection CLIENT KILL ID <id>.
  • URLs and env varsredis://, rediss://, unix://, REDIS_URL, REDIS_HOST, REDIS_PORT, REDIS_USERNAME, REDIS_PASSWORD, REDIS_DB, REDIS_TLS, REDIS_NAME.
  • Zero native dependencies. Pure TypeScript over Buffer, node:net, node:tls, node:crypto. Nothing to rebuild per platform.
  • GUI-shaped result surface. CommandReply carries the decoded value, the RESP type tag, and the raw RespValue for byte-exact round-trip in tools like Tusk.

Runtime targets

Runtime Status Notes
Perry (AOT → native) supported Same source, compiled via LLVM. No JS runtime at execution time.
Node.js ≥ 22 supported Uses node:net, node:tls, node:crypto, Buffer.
Bun ≥ 1.3 supported Fully works; TLS-from-SYN bypasses Bun's known mid-stream upgrade quirk.

Quickstart

Connecting

// Object form
const conn = await connect({
  host: '127.0.0.1', port: 6379,
  username: 'alice', password: 'acltestpw',  // ACL — both required
  database: 0,
  name: 'tusk-client-1',                    // CLIENT SETNAME via HELLO
});

// URL form (via `resolveConnectOptions`)
import { resolveConnectOptions } from '@perryts/redis';
const conn = await connect(resolveConnectOptions({
  url: 'redis://alice:acltestpw@db.internal:6379/0?name=tusk-client-1',
}));

// TLS
const conn = await connect(resolveConnectOptions({
  url: 'rediss://db.internal:6380?tls=verify-full',
}));

Pool

import { Pool } from '@perryts/redis';
const pool = new Pool({ host: '127.0.0.1', port: 6379, max: 10 });

// Acquire + run + release in one call:
const r = await pool.command('GET', 'key');

// Or manually:
await pool.withConnection(async (conn) => {
  await conn.command('MULTI');
  await conn.command('SET', 'a', '1');
  await conn.command('EXEC');
});

await pool.close();

Pipelining

const p = conn.pipeline();
for (let i = 0; i < 1000; i++) p.add('SET', 'k' + i, String(i));
const replies = await p.exec();
// replies[i] = { ok: true, value: CommandReply } | { ok: false, error: RedisError }

Pub/Sub

await conn.subscribe('news', (msg) => {
  console.log(msg.channel, msg.payload.toString('utf8'));
});

Transactions

const m = conn.multi();
m.add('SET', 'a', '1');
m.add('SET', 'b', '2');
const out = await m.exec();
// out[i] = { ok: true, value: CommandReply } | { ok: false, error } | { aborted: true }

Scripts

import { evalScript, evalShaScript, sha1Hex } from '@perryts/redis';
const sha1 = sha1Hex("return tonumber(ARGV[1]) * 2");
await evalScript(conn, "return tonumber(ARGV[1]) * 2", [], ['21']);
await evalShaScript(conn, sha1, [], ['21']);  // auto-retries on -NOSCRIPT

Cluster

import { ClusterClient } from '@perryts/redis';
const cluster = new ClusterClient({
  nodes: [
    { host: 'cluster1', port: 7001 },
    { host: 'cluster2', port: 7002 },
    { host: 'cluster3', port: 7003 },
  ],
  password: 'secret',
});
await cluster.command('SET', 'user:{1000}.profile', JSON.stringify({...}));

Development

bun install
bun test                    # unit + mock-server integration tests
bun run typecheck           # tsc --noEmit
bun run build               # tsc → dist/

# Live-server tests (requires a running Redis):
redis-server --port 36379 --daemonize yes
REDIS_HOST=127.0.0.1 REDIS_PORT=36379 bun test tests/integration/real-server.test.ts

License

MIT — see LICENSE.

About

Pure-TypeScript Redis / Valkey wire-protocol driver. Runs unchanged on Node.js + Bun, AOT-compiles to a native binary via Perry (LLVM).

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors