Keelv0.86.0
Docs / Redis

A Redis integration built on a small pluggable driver — like the database and mail layers, the core imports no client, so it runs on Node and on the edge. Point it at Upstash (HTTP/fetch), ioredis, node-redis, or the built-in MemoryRedis for tests and local dev.

Using it

Register a driver once (in a service provider), then reach Redis anywhere with redis():

import { redis, setRedis, MemoryRedis } from "@shaferllc/keel/core";

setRedis(new MemoryRedis()); // swap for an Upstash / ioredis adapter in production

await redis().set("views", "1");
await redis().incr("views");            // 2
await redis().get("views");             // "2"
await redis().set("token", "abc", { ex: 60 }); // expire in 60s
await redis().del("token");

The default client is a MemoryRedis, so redis() works out of the box in tests without any setup.

Commands

const r = redis();

await r.get(key);                 // string | null
await r.set(key, value, { ex });  // { ex: seconds } or { px: ms }
await r.del(...keys);             // number removed
await r.exists(...keys);          // number present
await r.has(key);                 // boolean

await r.incr(key);                // +1
await r.decr(key);                // -1
await r.incrBy(key, 5);

await r.expire(key, 60);          // set a TTL (seconds)
await r.ttl(key);                 // seconds left, -1 (no expiry), -2 (no key)
await r.keys("user:*");           // glob match
await r.flushAll();               // clear everything

JSON & remember

getJson / setJson handle serialization, and remember is the read-through cache pattern:

await redis().setJson("user:1", { id: 1, name: "Ada" });
const user = await redis().getJson<{ id: number; name: string }>("user:1");

// Compute once, cache for 300s, serve from cache after:
const stats = await redis().remember("stats", 300, () => computeStats());

As a cache store

redisStore() adapts the Redis client into a CacheStore, so the cache can be Redis-backed — shared across instances instead of per-process:

import { Cache, redisStore, redis } from "@shaferllc/keel/core";

const cache = new Cache(redisStore(redis()));
await cache.remember("home", 60, () => renderHome());

Writing a driver

A driver is the RedisConnection interface. Here's the shape for an Upstash REST client over fetch (edge-safe):

import type { RedisConnection } from "@shaferllc/keel/core";

const upstash = (url: string, token: string): RedisConnection => {
  const call = (...args: (string | number)[]) =>
    fetch(url, {
      method: "POST",
      headers: { Authorization: `Bearer ${token}` },
      body: JSON.stringify(args),
    }).then((r) => r.json());
  return {
    async get(key) { return (await call("GET", key)).result ?? null; },
    async set(key, value, o) {
      await (o?.ex ? call("SET", key, value, "EX", o.ex) : call("SET", key, value));
    },
    async del(...keys) { return (await call("DEL", ...keys)).result; },
    async exists(...keys) { return (await call("EXISTS", ...keys)).result; },
    async incrBy(key, n) { return (await call("INCRBY", key, n)).result; },
    async expire(key, s) { return (await call("EXPIRE", key, s)).result === 1; },
    async ttl(key) { return (await call("TTL", key)).result; },
    async keys(pattern) { return (await call("KEYS", pattern)).result; },
    async flushAll() { await call("FLUSHALL"); },
  };
};
setRedis(upstash(env("UPSTASH_URL"), env("UPSTASH_TOKEN")));

The optional queue commands

RedisConnection also declares eight optional methods — zadd, zrangebyscore, zrem, zcard, hset, hget, hgetall, hdel — the sorted-set and hash commands the queue's RedisDriver runs on. A minimal adapter that skips them still works everywhere else; the queue refuses it at first use with the missing commands named. Each maps 1:1 onto a standard Redis command, so extending the Upstash adapter above is one call(...) line apiece:

async zadd(key, score, member) { return (await call("ZADD", key, score, member)).result; },
async zrangebyscore(key, min, max, limit) {
  const args = limit === undefined ? [] : ["LIMIT", 0, limit];
  return (await call("ZRANGEBYSCORE", key, min, max, ...args)).result;
},
async zrem(key, member) { return (await call("ZREM", key, member)).result; },
// …and so on for zcard, hset, hget, hgetall, hdel

(For hgetall, mind your client's return shape — the queue expects a Record<string, string>; Upstash's REST API returns a flat [field, value, …] array you'll need to fold.)

In tests

MemoryRedis is a full in-memory implementation with TTL support — no server:

import { setRedis, MemoryRedis, redis } from "@shaferllc/keel/core";

setRedis(new MemoryRedis());
await redis().incr("signups");
assert.equal(await redis().get("signups"), "1");

API reference

redis()

redis(): Redis

The default client. Register a driver with setRedis first; defaults to a MemoryRedis.

setRedis(conn)

setRedis(conn: RedisConnection): Redis

Registers the driver behind redis() and returns the wrapping client. Last call wins.

Redis

Wraps a RedisConnection with conveniences.

Method Signature Notes
get (key) => Promise<string | null> raw string value
set (key, value, options?) => Promise<void> { ex: seconds } / { px: ms }
del / exists (...keys) => Promise<number> count removed / present
has (key) => Promise<boolean> exists(key) > 0
incr / decr (key) => Promise<number> ±1
incrBy (key, amount) => Promise<number> atomic add
expire (key, seconds) => Promise<boolean> false if the key is gone
ttl (key) => Promise<number> seconds, -1 no expiry, -2 no key
keys (pattern?) => Promise<string[]> glob; default "*"
flushAll () => Promise<void> clear all
getJson / setJson JSON convenience over get/set
remember (key, seconds, factory) => Promise<T> read-through cache

MemoryRedis

class MemoryRedis implements RedisConnection

An in-memory driver with TTL support — the default and ideal for tests. Not shared across processes.

redisStore(client?)

redisStore(client?: Redis): CacheStore

Adapts a Redis client into a CacheStore for the cache layer. Defaults to the global redis().

Interfaces & types

RedisConnection

The driver seam — implement it to back Redis with any client. Methods: get, set, del, exists, incrBy, expire, ttl, keys, flushAll.

SetOptions

interface SetOptions { ex?: number; px?: number }

Expiry for setex in seconds, px in milliseconds.