4.3 KiB
RedisLike
(no submodules)
Purpose
Declares the minimal Redis client surface RedisCache consumes. Any client conforming to this interface — ioredis, node-redis, a custom adapter over the official RESP3 protocol, or even a non-Redis store like KeyDB or Dragonfly that exposes the same commands — is a valid argument to the cache. Constraining the surface to seven primitives (set, get, del, exists, ttl, expire, scan) plus optional mget keeps the contract small enough that an in-memory FakeRedis for tests is straightforward to write and audit.
Features
- Seven required methods plus optional
mget— chosen to match the exact set of commandsRedisCacheneeds at runtime. setaccepts{ EX, PX }options matching the canonical Redis protocol; the cache only emitsEX.scanreturns the cursor + matched keys as a single normalised shape (RedisScanResult), letting adapters bridge over both theioredistuple shape[cursor, keys]and thenode-redisobject shape{ cursor, keys }.- No transactional, pub/sub, or scripting primitives — those concerns belong to the application, not the cache.
- Pure type module — zero runtime code, no
instanceofrequirement.
Usage: Basic Example
import type { RedisLike } from "@suaveplan/cache-redis/redis-like";
import { Redis } from "ioredis";
// `ioredis` v5 conforms to RedisLike out of the box.
const client: RedisLike = new Redis({ host: "redis.example.com" });
Usage: Advanced Example
import type {
RedisLike,
RedisScanResult,
} from "@suaveplan/cache-redis/redis-like";
import { createClient } from "redis";
// `node-redis` returns scan results in `{ cursor, keys }` shape and uses
// `pExpire` for millisecond TTLs — a thin adapter normalises both.
class NodeRedisAdapter implements RedisLike {
readonly #client = createClient();
async set(key: string, value: string, opts?: { EX?: number }) {
return opts?.EX !== undefined
? this.#client.set(key, value, { EX: opts.EX })
: this.#client.set(key, value);
}
async get(k: string) {
return this.#client.get(k);
}
async del(...keys: string[]) {
return this.#client.del(keys);
}
async exists(...keys: string[]) {
return this.#client.exists(keys);
}
async ttl(k: string) {
return this.#client.ttl(k);
}
async expire(k: string, s: number) {
const ok = await this.#client.expire(k, s);
return ok ? 1 : 0;
}
async scan(
cursor: string,
opts?: { MATCH?: string; COUNT?: number },
): Promise<RedisScanResult> {
const result = await this.#client.scan(Number(cursor), {
MATCH: opts?.MATCH,
COUNT: opts?.COUNT,
});
return { cursor: String(result.cursor), keys: result.keys };
}
}
API Reference
| Symbol | Kind | Description |
|---|---|---|
RedisLike |
interface | Required surface: set, get, del, exists, ttl, expire, scan, optional mget. |
RedisSetOptions |
interface | { EX?: number; PX?: number }. |
RedisScanOptions |
interface | { MATCH?: string; COUNT?: number }. |
RedisScanResult |
interface | { cursor: string; keys: string[] }. |
Method semantics
set(key, value, opts?)—SET key value [EX seconds]. Returns the server's response (typically"OK"); the cache ignores it.get(key)— returns the value ornullon miss.del(...keys)— returns the number of keys actually deleted.exists(...keys)— returns the number of matching keys.ttl(key)— returns seconds,-1for "no expiry",-2for "missing key".expire(key, seconds)— returns1on success,0on missing key.scan(cursor, opts?)— returns{ cursor, keys }withcursor === "0"signalling end of iteration.mget(...keys)— optional; if absent,getMultiplefalls back to per-keyget.
Implementation Notes
The interface uses string for both keys and values because the canonical RESP protocol speaks strings; RedisCache runs UTF-8 / base64 conversion at the boundary. Adapters that expose Buffer (ioredis in Buffer-mode, node-redis with returnBuffers) work as long as the adapter normalises back to string before returning. A future binary-safe variant of the cache may add a parallel RedisBinaryLike contract; until then keeping RedisLike string-only keeps the implementation universal compatible.