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