Table of Contents
The Functions
Aside from cold starts, often the slowest part of an application is fetching data. Caching data requests can speed up loading times. Using an internal store is one way to do it, or you can use a Redis cache. These functions can make working with a Redis cache extremely simple.
// $lib/server/cache.ts
import { REDIS_URL } from "$env/static/private";
import { Redis } from "ioredis";
const redis = new Redis(REDIS_URL);
/**
* A cache key as an array of strings with at least one element.
*/
export type CacheKey = [string, ...string[]];
/**
* Retrieves a cache from Redis or caches the results of a callback function.
* @template TReturnType The return type of the callback function.
* @param {() => Promise<TReturnType>} callback The callback function to cache.
* @param {CacheKey} key The cache key as an array of strings.
* @param {number} [expires=259200] The cache expiration time in seconds. Defaults to 3 days.
* @returns {Promise<TReturnType>} The cached result of the callback function.
*/
export async function cache<TReturnType>(callback: () => Promise<TReturnType>, key: CacheKey, expires = 3 * 86400) {
const rkey = key.join("|");
const currentTime = Date.now();
// Get the cache from Redis
const cache = JSON.parse((await redis.get(rkey)) || "null") as { data: TReturnType; timestamp: number } | null;
if (cache) {
// Update the timestamp and reset the cache expiration
cache.timestamp = currentTime;
redis.setex(rkey, expires, JSON.stringify(cache));
return cache.data;
}
// Call the callback function and cache the result
const result = await callback();
redis.setex(rkey, expires, JSON.stringify({ data: result, timestamp: currentTime }));
return result;
}
/**
* Retrieves caches from Redis or caches the results of a callback function for each key.
* @template TReturnType The return type of the callback function.
* @param {(key: CacheKey) => Promise<TReturnType>} callback The callback function to cache.
* @param {Array<CacheKey>} key The cache keys as an array of arrays of strings.
* @param {number} [expires=259200] The cache expiration time in seconds. Defaults to 3 days.
* @returns {Promise<Array<TReturnType>>} An array of cached results of the callback function for each key.
*/
export async function mcache<TReturnType>(
callback: (key: CacheKey) => Promise<TReturnType>,
key: Array<CacheKey>,
expires = 3 * 86400
) {
const keys = key.map((t) => t.join("|"));
// Get the caches from Redis
const caches = await redis.mget(keys);
const results: Array<TReturnType> = [];
for (let i = 0; i < caches.length; i++) {
const currentTime = Date.now();
const value = caches[i];
if (value) {
const cache: { data: TReturnType; timestamp: number } = JSON.parse(value);
// Update the timestamp and reset the cache expiration
cache.timestamp = currentTime;
redis.setex(keys[i], expires, JSON.stringify(cache));
// Add the cached result to the results array
results[i] = cache.data;
continue;
}
// Call the callback function and cache the result
const result = await callback(key[i]);
redis.setex(keys[i], expires, JSON.stringify({ data: result, timestamp: currentTime }));
// Add the result to the results array
results[i] = result;
}
return results;
}
/**
* Invalidates Redis caches based on an array of keys.
* @param {Array<CacheKey | "" | false | null | undefined>} keys The cache keys as an array of arrays of strings. Empty strings, false, null, and undefined are ignored.
* @returns {void}
*/
export function revalidateKeys(keys: Array<CacheKey | "" | false | null | undefined>) {
const cacheKeys = keys.filter((t) => Array.isArray(t) && t.length).map((t) => (t as string[]).join("|"));
if (cacheKeys.length) redis.del(...cacheKeys);
}
Example
To show how simple these functions are to use, here is an example:
import { cache, mcache, type CacheKey } from "$/server/cache";
import { prisma } from "$/server/db";
// A function which fetches data from a DB or API
export async function getModel(id: string): Model | null {
return await prisma.model.findFirst({
where: { id }
});
}
// A function which checks for and returns a cache
// or calls a callback function and fetches and returns
// the result.
export async function getModelCache(id: string) {
return await cache(() => getModel(id), ["model", id]);
// ^? Model | null
}
// A function which checks for and returns multiple caches
// or calls a callback function and fetches and returns the
// result for each key.
export async function getModelCaches(ids: string[]) {
const keys: Array<CacheKey> = ids.map((id) => ["model", id]);
return await cache((key) => getModel(key[1]), keys);
// ^? Array<Model | null>
}