Skip to content

Instantly share code, notes, and snippets.

@saiashirwad
Created May 25, 2023 11:10
Show Gist options
  • Save saiashirwad/f69504f2588dafd512b8772cac96ba5b to your computer and use it in GitHub Desktop.
Save saiashirwad/f69504f2588dafd512b8772cac96ba5b to your computer and use it in GitHub Desktop.
super basic cache setup
import { type Duration } from "date-fns";
// RedisClientType is super chonky and I can't be bothered to lug that around everywhere
export type RedisClient = {
get: (key: string) => Promise<string | null>;
set: (key: string, value: string) => Promise<void>;
mSet: (records: Record<string, string>) => Promise<void>;
del: (key: string) => Promise<void>;
mGet: (keys: string[]) => Promise<string[]>;
connect: () => Promise<void>;
};
export type Cache<T> = {
get: (key: string) => Promise<T | null>;
getMany: (keys: string[]) => Promise<Record<string, T | null>>;
refetch: (key: string) => Promise<T | null>;
set: (key: string, value: T) => Promise<void>;
setMany: (records: Record<string, T>) => Promise<void>;
del: (key: string) => Promise<void>;
/**
* Only works if `populateFn` is provided
*
* Maybe set up a cron job to run this every x hours?
*/
populate: () => Promise<void>;
};
type CacheOptions = {
ttl?: Duration;
};
// TODO: implement TTL
type CacheBuilder<T> = (
client: RedisClient,
options?: CacheOptions,
) => Cache<T>;
export const createCache =
<T>(
prefix: string,
fn: (keys: string[]) => Promise<Record<string, T>>,
populateFn?: () => Promise<Record<string, T>>,
): CacheBuilder<T> =>
(client: RedisClient, options?: CacheOptions): Cache<T> => {
// NOTE: Would it be *REALLY* stupid if I just stored the last updated
// timestamp in redis?
const lastUpdated: Record<string, Date> = {};
const populate = async () => {
if (populateFn) {
const records = await populateFn();
await setMany(records);
}
};
const get = async (key: string): Promise<T | null> => {
const value = await client.get(`${prefix}:${key}`);
if (value === null) {
const result = (await fn([key]))[key];
if (result) {
await client.set(`${prefix}:${key}`, JSON.stringify(result));
} else {
return null;
}
return result;
}
return JSON.parse(value) as T;
};
const refetch = async (key: string): Promise<T | null> => {
const result = (await fn([key]))[key];
if (result) {
await client.set(`${prefix}:${key}`, JSON.stringify(result));
return result;
} else {
return null;
}
};
const set = async (key: string, value: T) => {
await client.set(`${prefix}:${key}`, JSON.stringify(value));
};
const setMany = async (records: Record<string, T>) => {
const data = Object.fromEntries(
Object.entries(records).map(([key, value]) => [
`${prefix}:${key}`,
JSON.stringify(value),
]),
);
await client.mSet(data);
};
const del = async (key: string) => {
await client.del(`${prefix}:${key}`);
};
const getMany = async (
keys: string[],
): Promise<Record<string, T | null>> => {
const result = await client.mGet(keys.map((key) => `${prefix}:${key}`));
const missingIdxs: number[] = [];
for (const r of result) {
if (r === null) {
missingIdxs.push(result.indexOf(r));
}
}
if (missingIdxs.length > 0) {
const missingKeys = missingIdxs.map((idx) => keys[idx]) as string[];
const missingRecords = await fn(missingKeys);
if (Object.keys(missingRecords).length !== 0) {
await setMany(missingRecords);
}
for (const idx of missingIdxs) {
result[idx] = JSON.stringify(missingRecords[keys[idx] as string]);
}
}
const data = result.map((r, idx) => [
keys[idx],
r ? JSON.parse(r) : null,
]) as [string, T | null][];
return Object.fromEntries(data);
};
return { get, refetch, set, del, setMany, getMany, populate };
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment