import cacache from 'cacache'; import { Readable } from 'stream'; function getBodyAndMetaKeys(key: string): [string, string] { return [`${key}body`, `${key}meta`]; } export class FileSystemCache { constructor(public options: { ttl?: number; cacheDirectory: string }) {} async get(key: string) { const [, metaKey] = getBodyAndMetaKeys(key); const metaInfo = await cacache.get.info(this.options.cacheDirectory, metaKey); if (!metaInfo) { return undefined; } const metaBuffer = await cacache.get.byDigest(this.options.cacheDirectory, metaInfo.integrity); const metaData = JSON.parse(metaBuffer); const { bodyStreamIntegrity, empty, expiration } = metaData; delete metaData.bodyStreamIntegrity; delete metaData.empty; delete metaData.expiration; if (expiration && expiration < Date.now()) { return undefined; } const bodyStream = empty ? Readable.from(Buffer.alloc(0)) : cacache.get.stream.byDigest(this.options.cacheDirectory, bodyStreamIntegrity); return { bodyStream, metaData, }; } remove(key: string) { const [bodyKey, metaKey] = getBodyAndMetaKeys(key); return Promise.all([ cacache.rm.entry(this.options.cacheDirectory, bodyKey), cacache.rm.entry(this.options.cacheDirectory, metaKey), ]); } async set(key: string, bodyStream: NodeJS.ReadStream, metaData: any) { const [bodyKey, metaKey] = getBodyAndMetaKeys(key); const metaCopy = { ...metaData }; if (typeof this.options.ttl === 'number') { metaCopy.expiration = Date.now() + this.options.ttl; } try { metaCopy.bodyStreamIntegrity = await new Promise((fulfill, reject) => { bodyStream .pipe(cacache.put.stream(this.options.cacheDirectory, bodyKey)) .on('integrity', (i) => fulfill(i)) .on('error', (e) => { reject(e); }); }); } catch (err: any) { if (err.code !== 'ENODATA') { throw err; } metaCopy.empty = true; } const metaBuffer = Buffer.from(JSON.stringify(metaCopy)); await cacache.put(this.options.cacheDirectory, metaKey, metaBuffer); const cachedData = await this.get(key); return cachedData; } }