Drivers
Custom Drivers
Implement your own cache driver for any backend by extending CacheDriver
Interface
import { CacheDriver, type CacheEntry } from "viborm";
abstract class CacheDriver {
// Get a cache entry by key
protected abstract get<T>(key: string): Promise<CacheEntry<T> | null>;
// Store a cache entry with TTL
protected abstract set<T>(
key: string,
storageTtl: number, // milliseconds
entry: CacheEntry<T>
): Promise<void>;
// Delete specific keys
protected abstract delete(keys: string[]): Promise<void>;
// Clear all keys with a given prefix
protected abstract clear(prefix: string): Promise<void>;
}CacheEntry Type
interface CacheEntry<T = unknown> {
value: T; // The cached data
createdAt: number; // Timestamp when cached (Date.now())
ttl: number; // Original TTL in milliseconds
}Redis Example
import { CacheDriver, type CacheEntry } from "viborm";
import { Redis } from "ioredis";
export class RedisCache extends CacheDriver {
private readonly redis: Redis;
constructor(redis: Redis) {
super("redis");
this.redis = redis;
}
protected async get<T>(key: string): Promise<CacheEntry<T> | null> {
const data = await this.redis.get(key);
if (!data) return null;
return JSON.parse(data) as CacheEntry<T>;
}
protected async set<T>(
key: string,
storageTtl: number,
entry: CacheEntry<T>
): Promise<void> {
// Redis PSETEX takes TTL in milliseconds
await this.redis.psetex(key, storageTtl, JSON.stringify(entry));
}
protected async delete(keys: string[]): Promise<void> {
if (keys.length > 0) {
await this.redis.del(...keys);
}
}
protected async clear(prefix: string): Promise<void> {
// Use SCAN to find keys (avoid KEYS in production)
const stream = this.redis.scanStream({
match: `${prefix}*`,
count: 100,
});
const pipeline = this.redis.pipeline();
for await (const keys of stream) {
for (const key of keys as string[]) {
pipeline.del(key);
}
}
await pipeline.exec();
}
}Usage
import { createClient } from "viborm";
import { Redis } from "ioredis";
import { RedisCache } from "./redis-cache";
const redis = new Redis(process.env.REDIS_URL);
const client = createClient({
schema: { user, post },
driver,
cache: new RedisCache(redis),
});Upstash Redis Example
For serverless environments, Upstash provides a REST-based Redis:
import { CacheDriver, type CacheEntry } from "viborm";
import { Redis } from "@upstash/redis";
export class UpstashCache extends CacheDriver {
private readonly redis: Redis;
constructor(redis: Redis) {
super("upstash");
this.redis = redis;
}
protected async get<T>(key: string): Promise<CacheEntry<T> | null> {
const data = await this.redis.get<CacheEntry<T>>(key);
return data ?? null;
}
protected async set<T>(
key: string,
storageTtl: number,
entry: CacheEntry<T>
): Promise<void> {
// Upstash px option takes milliseconds
await this.redis.set(key, entry, { px: storageTtl });
}
protected async delete(keys: string[]): Promise<void> {
if (keys.length > 0) {
await this.redis.del(...keys);
}
}
protected async clear(prefix: string): Promise<void> {
let cursor = 0;
do {
const [nextCursor, keys] = await this.redis.scan(cursor, {
match: `${prefix}*`,
count: 100,
});
cursor = Number(nextCursor);
if (keys.length > 0) {
await this.redis.del(...keys);
}
} while (cursor !== 0);
}
}Usage with Vercel
import { createClient } from "viborm";
import { Redis } from "@upstash/redis";
import { waitUntil } from "@vercel/functions";
import { UpstashCache } from "./upstash-cache";
const redis = Redis.fromEnv();
const client = createClient({
schema: { user },
driver,
cache: new UpstashCache(redis),
waitUntil,
});Implementation Guidelines
1. Call super() with a Driver Name
constructor(/* config */) {
super("my-driver"); // Used for logging/debugging
// ...
}2. Handle Missing Keys
Return null when a key doesn't exist, not an error:
protected async get<T>(key: string): Promise<CacheEntry<T> | null> {
const data = await this.backend.get(key);
return data ?? null; // null, not undefined
}3. Respect Storage TTL
The storageTtl parameter is already calculated (2x user TTL for SWR support). Use it directly:
protected async set<T>(key: string, storageTtl: number, entry: CacheEntry<T>) {
// storageTtl is in milliseconds
await this.backend.set(key, entry, { ttl: storageTtl });
}4. Handle Empty Operations Gracefully
protected async delete(keys: string[]): Promise<void> {
if (keys.length === 0) return; // No-op for empty array
await this.backend.del(...keys);
}5. Prefix Clearing Should Match All
The clear method receives a prefix like viborm:user. It should delete all keys starting with that prefix:
protected async clear(prefix: string): Promise<void> {
// Match: viborm:user:findMany:abc, viborm:user:findFirst:xyz, etc.
const keys = await this.backend.keys(`${prefix}*`);
// ...
}Testing Your Driver
import { describe, it, expect } from "vitest";
import { MyCache } from "./my-cache";
describe("MyCache", () => {
it("stores and retrieves entries", async () => {
const cache = new MyCache(/* config */);
await cache._set("test-key", { id: 1 }, { ttl: 60000 });
const entry = await cache._get("test-key");
expect(entry?.value).toEqual({ id: 1 });
});
it("returns null for missing keys", async () => {
const cache = new MyCache(/* config */);
const entry = await cache._get("nonexistent");
expect(entry).toBeNull();
});
it("clears by prefix", async () => {
const cache = new MyCache(/* config */);
await cache._set("viborm:user:a", {}, { ttl: 60000 });
await cache._set("viborm:user:b", {}, { ttl: 60000 });
await cache._set("viborm:post:c", {}, { ttl: 60000 });
await cache._invalidate("user", {});
expect(await cache._get("viborm:user:a")).toBeNull();
expect(await cache._get("viborm:user:b")).toBeNull();
expect(await cache._get("viborm:post:c")).not.toBeNull();
});
});