Files
skills/docs/persistence/cache/cache-redis/src/redis-like/redis-like.md
2026-05-29 08:20:56 +01:00

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 commands RedisCache needs at runtime.
  • set accepts { EX, PX } options matching the canonical Redis protocol; the cache only emits EX.
  • scan returns the cursor + matched keys as a single normalised shape (RedisScanResult), letting adapters bridge over both the ioredis tuple shape [cursor, keys] and the node-redis object 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 instanceof requirement.

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 or null on miss.
  • del(...keys) — returns the number of keys actually deleted.
  • exists(...keys) — returns the number of matching keys.
  • ttl(key) — returns seconds, -1 for "no expiry", -2 for "missing key".
  • expire(key, seconds) — returns 1 on success, 0 on missing key.
  • scan(cursor, opts?) — returns { cursor, keys } with cursor === "0" signalling end of iteration.
  • mget(...keys) — optional; if absent, getMultiple falls back to per-key get.

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.