1*8d307f52SEvan Baconimport cacache from 'cacache'; 2*8d307f52SEvan Baconimport { Readable } from 'stream'; 3*8d307f52SEvan Bacon 4*8d307f52SEvan Baconfunction getBodyAndMetaKeys(key: string): [string, string] { 5*8d307f52SEvan Bacon return [`${key}body`, `${key}meta`]; 6*8d307f52SEvan Bacon} 7*8d307f52SEvan Bacon 8*8d307f52SEvan Baconexport class FileSystemCache { 9*8d307f52SEvan Bacon constructor(public options: { ttl?: number; cacheDirectory: string }) {} 10*8d307f52SEvan Bacon 11*8d307f52SEvan Bacon async get(key: string) { 12*8d307f52SEvan Bacon const [, metaKey] = getBodyAndMetaKeys(key); 13*8d307f52SEvan Bacon 14*8d307f52SEvan Bacon const metaInfo = await cacache.get.info(this.options.cacheDirectory, metaKey); 15*8d307f52SEvan Bacon 16*8d307f52SEvan Bacon if (!metaInfo) { 17*8d307f52SEvan Bacon return undefined; 18*8d307f52SEvan Bacon } 19*8d307f52SEvan Bacon 20*8d307f52SEvan Bacon const metaBuffer = await cacache.get.byDigest(this.options.cacheDirectory, metaInfo.integrity); 21*8d307f52SEvan Bacon const metaData = JSON.parse(metaBuffer); 22*8d307f52SEvan Bacon const { bodyStreamIntegrity, empty, expiration } = metaData; 23*8d307f52SEvan Bacon 24*8d307f52SEvan Bacon delete metaData.bodyStreamIntegrity; 25*8d307f52SEvan Bacon delete metaData.empty; 26*8d307f52SEvan Bacon delete metaData.expiration; 27*8d307f52SEvan Bacon 28*8d307f52SEvan Bacon if (expiration && expiration < Date.now()) { 29*8d307f52SEvan Bacon return undefined; 30*8d307f52SEvan Bacon } 31*8d307f52SEvan Bacon 32*8d307f52SEvan Bacon const bodyStream = empty 33*8d307f52SEvan Bacon ? Readable.from(Buffer.alloc(0)) 34*8d307f52SEvan Bacon : cacache.get.stream.byDigest(this.options.cacheDirectory, bodyStreamIntegrity); 35*8d307f52SEvan Bacon 36*8d307f52SEvan Bacon return { 37*8d307f52SEvan Bacon bodyStream, 38*8d307f52SEvan Bacon metaData, 39*8d307f52SEvan Bacon }; 40*8d307f52SEvan Bacon } 41*8d307f52SEvan Bacon 42*8d307f52SEvan Bacon remove(key: string) { 43*8d307f52SEvan Bacon const [bodyKey, metaKey] = getBodyAndMetaKeys(key); 44*8d307f52SEvan Bacon 45*8d307f52SEvan Bacon return Promise.all([ 46*8d307f52SEvan Bacon cacache.rm.entry(this.options.cacheDirectory, bodyKey), 47*8d307f52SEvan Bacon cacache.rm.entry(this.options.cacheDirectory, metaKey), 48*8d307f52SEvan Bacon ]); 49*8d307f52SEvan Bacon } 50*8d307f52SEvan Bacon 51*8d307f52SEvan Bacon async set(key: string, bodyStream: NodeJS.ReadStream, metaData: any) { 52*8d307f52SEvan Bacon const [bodyKey, metaKey] = getBodyAndMetaKeys(key); 53*8d307f52SEvan Bacon const metaCopy = { ...metaData }; 54*8d307f52SEvan Bacon 55*8d307f52SEvan Bacon if (typeof this.options.ttl === 'number') { 56*8d307f52SEvan Bacon metaCopy.expiration = Date.now() + this.options.ttl; 57*8d307f52SEvan Bacon } 58*8d307f52SEvan Bacon 59*8d307f52SEvan Bacon try { 60*8d307f52SEvan Bacon metaCopy.bodyStreamIntegrity = await new Promise((fulfill, reject) => { 61*8d307f52SEvan Bacon bodyStream 62*8d307f52SEvan Bacon .pipe(cacache.put.stream(this.options.cacheDirectory, bodyKey)) 63*8d307f52SEvan Bacon .on('integrity', (i) => fulfill(i)) 64*8d307f52SEvan Bacon .on('error', (e) => { 65*8d307f52SEvan Bacon reject(e); 66*8d307f52SEvan Bacon }); 67*8d307f52SEvan Bacon }); 68*8d307f52SEvan Bacon } catch (err: any) { 69*8d307f52SEvan Bacon if (err.code !== 'ENODATA') { 70*8d307f52SEvan Bacon throw err; 71*8d307f52SEvan Bacon } 72*8d307f52SEvan Bacon 73*8d307f52SEvan Bacon metaCopy.empty = true; 74*8d307f52SEvan Bacon } 75*8d307f52SEvan Bacon 76*8d307f52SEvan Bacon const metaBuffer = Buffer.from(JSON.stringify(metaCopy)); 77*8d307f52SEvan Bacon await cacache.put(this.options.cacheDirectory, metaKey, metaBuffer); 78*8d307f52SEvan Bacon const cachedData = await this.get(key); 79*8d307f52SEvan Bacon 80*8d307f52SEvan Bacon return cachedData; 81*8d307f52SEvan Bacon } 82*8d307f52SEvan Bacon} 83