Created
May 25, 2023 11:10
-
-
Save saiashirwad/f69504f2588dafd512b8772cac96ba5b to your computer and use it in GitHub Desktop.
super basic cache setup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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