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