import path from 'node:path'
import { FileManager } from './CLI'
export const ttlMap = {
n_seconds: (n: number) => 1000 * n,
n_minutes: (n: number) => 1000 * 60 * n,
n_hours: (n: number) => 1000 * 60 * 60 * n,
n_days: (n: number) => 1000 * 60 * 60 * 24 * n,
n_weeks: (n: number) => 1000 * 60 * 60 * 24 * 7 * n,
n_months: (n: number) => 1000 * 60 * 60 * 24 * 30 * n,
n_years: (n: number) => 1000 * 60 * 60 * 24 * 365 * n,
}
class Cacher<T> {
protected cache: Map<string, T> = new Map()
protected cacheWithTTL: Map<string, { value: T, expiresAt: Date }> = new Map()
constructor(protected ttl: number = ttlMap.n_days(1)) {
}
setTTL(ttl: number): void {
this.ttl = ttl
}
getWithTTL(key: string): T | undefined {
const cached = this.cacheWithTTL.get(key)
if (cached) {
if (cached.expiresAt > new Date()) {
return cached.value
}
else {
this.cacheWithTTL.delete(key)
return cached.value
}
}
return undefined
}
setWithTTL(key: string, value: T, ttl: number): void {
this.cacheWithTTL.set(key, { value, expiresAt: new Date(Date.now() + ttl) })
}
get(key: string): T | undefined {
return this.cache.get(key)
}
set(key: string, value: T): void {
this.cache.set(key, value)
}
delete(key: string): void {
this.cache.delete(key)
}
clear(): void {
this.cache.clear()
this.cacheWithTTL.clear()
}
size(): number {
return this.cache.size
}
keys(): string[] {
return Array.from(this.cache.keys())
}
values(): T[] {
return Array.from(this.cache.values())
}
valuesWithTTL(): { value: T, expiresAt: Date }[] {
return Array.from(this.cacheWithTTL.values())
}
}
export class Base64FileCacher extends Cacher<string> {
private cacheDirCreated = false
private withTTL: boolean = false
constructor(private cacheDir: string, ttl: number | undefined) {
super(ttl)
if (ttl) {
this.withTTL = true
}
}
private getCachePath(value: string): string {
return path.join(this.cacheDir, value)
}
private async upsertCacheDirectory() {
if (!this.cacheDirCreated) {
await FileManager.createDirectory(this.cacheDir, { overwrite: false })
this.cacheDirCreated = true
}
}
private async getCacheFile(key: string, options?: { withTTL: boolean }): Promise<string | null> {
await this.upsertCacheDirectory()
const value = options?.withTTL ? this.getWithTTL(key) : this.get(key)
if (!value) {
return null
}
const cachePath = this.getCachePath(value)
try {
const content = await FileManager.readFileAsBase64(cachePath)
if (!content) {
return null
}
else {
console.log(`Cache hit for ${key}`)
return content
}
}
catch (error) {
console.error(`Error reading cache file ${cachePath}:`, error)
return null
}
}
private async writeCacheFile(key: string, base64String: string, options?: { withTTL: boolean }) {
await this.upsertCacheDirectory()
const filename = options?.withTTL ? this.getWithTTL(key) : this.get(key)
if (!filename) {
return
}
const cachePath = this.getCachePath(filename)
await FileManager.createFileFromBase64(cachePath, base64String, { overwrite: true })
}
async getCacheFirst(key: string, options?: {
getBase64Revalidate: () => Promise<{
filename: string
base64String: string
} | null>
}): Promise<string | null> {
const cachedBase64 = await this.getCacheFile(key, { withTTL: this.withTTL })
if (cachedBase64) {
return cachedBase64
}
const data = await options?.getBase64Revalidate()
if (!data) {
return null
}
else {
this.addFilenameToCache(key, data.filename)
await this.writeCacheFile(key, data.base64String, { withTTL: this.withTTL })
return data.base64String
}
}
addFilenameToCache(key: string, filename: string) {
if (this.withTTL) {
this.setWithTTL(key, filename, this.ttl)
}
else {
this.set(key, filename)
}
}
}
export class ObjectCacher<T> extends Cacher<T> {
async getCacheFirst(key: string, revalidate: () => Promise<T | null>): Promise<T | null> {
const cached = this.get(key)
if (cached) {
return cached
}
const value = await revalidate()
if (!value) {
return null
}
else {
this.set(key, value)
return value
}
}
async getWithRevalidate(key: string, revalidate: () => Promise<T | null>): Promise<T | null> {
const cached = this.get(key)
if (cached) {
const value = await revalidate()
if (value) {
this.set(key, value)
return value
}
else {
return cached
}
}
const value = await revalidate()
if (!value) {
return null
}
else {
this.set(key, value)
return value
}
}
}
export class QueryCacher<T> extends Cacher<T> {
constructor(private exec: (query: string) => Promise<T | null>) {
super()
}
async getCacheFirst(key: string): Promise<T | null> {
const cached = this.get(key)
if (cached) {
return cached
}
const value = await this.exec(key)
if (!value) {
return null
}
else {
this.set(key, value)
return value
}
}
async getWithRevalidate(key: string): Promise<T | null> {
const cached = this.get(key)
if (cached) {
const value = await this.exec(key)
if (value) {
this.set(key, value)
return value
}
else {
return cached
}
}
const value = await this.exec(key)
if (!value) {
return null
}
else {
this.set(key, value)
return value
}
}
}