Last active
April 19, 2023 04:54
-
-
Save kentcdodds/0b68f47e8a4d29a8952e4701865f73d6 to your computer and use it in GitHub Desktop.
Turn any function into a cachified one. With forceFresh support and value checking (for when the data type changes). This uses redis, but you could change it to use whatever you want.
This file contains 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
type CacheMetadata = { | |
createdTime: number | |
maxAge: number | null | |
expires: number | null | |
} | |
function shouldRefresh(metadata: CacheMetadata) { | |
if (metadata.maxAge) { | |
return Date.now() > metadata.createdTime + metadata.maxAge | |
} | |
if (metadata.expires) { | |
return Date.now() > metadata.expires | |
} | |
return false | |
} | |
async function cachified<ReturnValue>(options: { | |
key: string | |
getFreshValue: () => Promise<ReturnValue> | |
checkValue?: (value: ReturnValue) => boolean | |
forceFresh?: boolean | |
request?: Request | |
fallbackToCache?: boolean | |
timings?: Timings | |
timingType?: string | |
maxAge?: number | |
expires?: Date | |
}): Promise<ReturnValue> { | |
const { | |
key, | |
getFreshValue, | |
request, | |
forceFresh = request ? await shouldForceFresh(request) : false, | |
checkValue = value => Boolean(value), | |
fallbackToCache = true, | |
timings, | |
timingType = 'getting fresh value', | |
maxAge, | |
expires, | |
} = options | |
if (!forceFresh) { | |
try { | |
const cached = await time({ | |
name: `redis.get(${key})`, | |
type: 'redis cache read', | |
fn: () => get(key), | |
timings, | |
}) | |
if (cached) { | |
const cachedParsed = JSON.parse(cached) as { | |
metadata?: CacheMetadata | |
value?: ReturnValue | |
} | |
if (cachedParsed.metadata && shouldRefresh(cachedParsed.metadata)) { | |
// time to refresh the value. Fire and forget so we don't slow down | |
// this request | |
// originally I thought it may be good to make sure we don't have | |
// multiple requests for the same key triggering multiple refreshes | |
// like this, but as I thought about it I realized the liklihood of | |
// this causing real issues is pretty small (unless there's a failure) | |
// to update the value, in which case we should probably be notified | |
// anyway... | |
void cachified({...options, forceFresh: true}) | |
} | |
if (cachedParsed.value && checkValue(cachedParsed.value)) { | |
return cachedParsed.value | |
} else { | |
console.warn( | |
`check failed for cached value of ${key}. Deleting the cache key and trying to get a fresh value.`, | |
cachedParsed, | |
) | |
await del(key) | |
} | |
} | |
} catch (error: unknown) { | |
console.error(`error with cache at ${key}`, getErrorMessage(error)) | |
} | |
} | |
const value = await time({ | |
name: `getFreshValue for ${key}`, | |
type: timingType, | |
fn: getFreshValue, | |
timings, | |
}).catch((error: unknown) => { | |
// If we got this far without forceFresh then we know there's nothing | |
// in the cache so no need to bother. So we need both the option to fallback | |
// and the ability. | |
if (fallbackToCache && forceFresh) { | |
return cachified({...options, forceFresh: false}) | |
} else { | |
throw error | |
} | |
}) | |
if (checkValue(value)) { | |
const metadata: CacheMetadata = { | |
maxAge: maxAge ?? null, | |
expires: expires?.getTime() ?? null, | |
createdTime: Date.now(), | |
} | |
void set(key, JSON.stringify({metadata, value})).catch(error => { | |
console.error(`error setting redis.${key}`, getErrorMessage(error)) | |
}) | |
} else { | |
console.error(`check failed for fresh value of ${key}:`, value) | |
throw new Error(`check failed for fresh value of ${key}`) | |
} | |
return value | |
} | |
async function shouldForceFresh(request: Request) { | |
return ( | |
new URL(request.url).searchParams.has('fresh') && | |
(await getUser(request))?.role === 'ADMIN' | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment