Last active
February 11, 2025 15:15
-
-
Save blakek/e6ef5c3425d720d41822444463cf7f86 to your computer and use it in GitHub Desktop.
A very simple cache that stores a value and its expiration time in a file
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
// Uses Bun, but this could quite easily use any storage (Node.js fs, in-browser `localStorage`, etc.) | |
import * as Bun from "bun"; | |
import * as fs from "node:fs/promises"; | |
export interface Expirable<T> { | |
value: T; | |
expires: number | null; | |
} | |
export interface CacheOptions { | |
cacheKey: string; | |
timeToLive: number | null; | |
} | |
/** | |
* A very simple cache that stores a value and its expiration time in a file. | |
*/ | |
export class SimpleCache<T> { | |
private cacheFilePath: URL; | |
private cacheFile: Bun.BunFile; | |
private timeToLive: number | null; | |
constructor(cacheKey: string, timeToLive: number | null = null) { | |
// I'm storing this in an ignored folder near the implementation because that's easiest for me. | |
// This could be stored anywhere… remotely, as an in-memory object, whatever. | |
this.cacheFilePath = new URL(`.cache/${cacheKey}.json`, import.meta.url); | |
this.cacheFile = Bun.file(this.cacheFilePath); | |
this.timeToLive = timeToLive; | |
} | |
private async createCacheFile(): Promise<void> { | |
await fs.appendFile(this.cacheFilePath, ""); | |
} | |
private async hasCacheFile(): Promise<boolean> { | |
return fs.exists(this.cacheFilePath); | |
} | |
private readCacheFile(): Promise<Expirable<T>> { | |
return this.cacheFile.json(); | |
} | |
private writeCacheFile(cache: Expirable<T>): void { | |
this.cacheFile.writer().write(JSON.stringify(cache)); | |
} | |
async clear(): Promise<void> { | |
return fs.rm(this.cacheFilePath, { force: true }); | |
} | |
async get(): Promise<T | null> { | |
if (!(await this.hasCacheFile())) { | |
return null; | |
} | |
let cache: Expirable<T> | null = null; | |
try { | |
cache = await this.readCacheFile(); | |
} catch { | |
console.warn("Failed to read cache file"); | |
} | |
if (!cache) { | |
return null; | |
} | |
if ("expires" in cache === false || "value" in cache === false) { | |
console.warn("Invalid cache format"); | |
return null; | |
} | |
if (cache.expires !== null && cache.expires < Date.now()) { | |
return null; | |
} | |
return cache.value; | |
} | |
async refresh( | |
customDuration: number | null = this.timeToLive | |
): Promise<void> { | |
if (!(await this.hasCacheFile())) { | |
return; | |
} | |
const cache = await this.readCacheFile(); | |
cache.expires = | |
customDuration === null ? null : Date.now() + customDuration; | |
this.writeCacheFile(cache); | |
} | |
async set(value: T): Promise<void> { | |
if (!(await this.hasCacheFile())) { | |
await this.createCacheFile(); | |
} | |
const cache: Expirable<T> = { | |
value, | |
expires: this.timeToLive === null ? null : Date.now() + this.timeToLive, | |
}; | |
const serializedCache = JSON.stringify(cache); | |
this.cacheFile.writer().write(serializedCache); | |
} | |
} | |
export function withCache< | |
Fn extends (...args: any) => Promise<any>, | |
Return = ReturnType<Fn>, | |
Args extends Parameters<Fn> = Parameters<Fn> | |
>( | |
fn: Fn, | |
options: Partial<CacheOptions> = {} | |
): (...args: Args) => Promise<Return> { | |
const { cacheKey = fn.name, timeToLive = null } = options; | |
const cache = new SimpleCache<Return>(cacheKey, timeToLive); | |
return async (...args: Args): Promise<Return> => { | |
const cachedValue = await cache.get(); | |
if (cachedValue !== null) { | |
return cachedValue; | |
} | |
const result = await fn(...args); | |
await cache.set(result); | |
return result; | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment