UNPKG

@stencila/jesta

Version:

Stencila plugin for executable documents using JavaScript

125 lines (124 loc) 4.17 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.cache = exports.Cache = void 0; const crypto_1 = __importDefault(require("crypto")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const util_1 = require("util"); const dirs_1 = require("../util/dirs"); /** * Cache class that implements the methods required to * act as a [Keyv](https://github.com/lukechilds/keyv) storage adapter. * * Stores values as files in a Stencila cache directory intended to * be used across applications and plugins. Removes the least * recently accessed files to maintain the size of the cache below a * defined size. * * Note that this differs from https://github.com/zaaack/keyv-file in that * if persists values across processes. This is important for CLI applications * since you do not want to create a new cache for every invocation (as * does `keyv-file` by default). */ class Cache { /** * Create the cache. */ constructor() { /** * The maximum size of the cache (MiB). */ this.maximumSize = 100; /** * The amount of time between cleanup checks (seconds). */ this.cleanupInterval = 120; this.dir = dirs_1.cache(); } /** * Generates a file name within the cache directory. * * Use a hash to avoid invalid characters and names * that are too long. Use SHA1 because faster than SHA256 * and does not need to be secure. */ filename(key) { const filename = crypto_1.default.createHash('sha1').update(key).digest('hex'); return path_1.default.join(this.dir, filename); } /** * Set a value to be cached. */ set(key, value) { fs_1.default.writeFileSync(this.filename(key), value, 'utf8'); process.nextTick(() => this.cleanup()); } /** * Get a value from the cache. */ get(key) { try { return fs_1.default.readFileSync(this.filename(key), 'utf8'); } catch (error) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (error.code === 'ENOENT') return undefined; throw error; } } /** * Delete a value from the cache. */ delete(key) { try { fs_1.default.unlinkSync(this.filename(key)); } catch (error) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (error.code !== 'ENOENT') throw error; } return true; } /** * Clear the cache completely. */ clear() { return fs_1.default.rmdirSync(this.dir, { recursive: true }); } /** * If the cache directory has gone over the maximum size remove * the files that are least recently accessed. */ cleanup() { const doit = async () => { const names = await util_1.promisify(fs_1.default.readdir)(this.dir); const files = await Promise.all(names.map(async (name) => { const path_ = path_1.default.join(this.dir, name); const stats = await util_1.promisify(fs_1.default.stat)(path_); return { path: path_, stats, }; })); const total = files.reduce((prev, file) => prev + file.stats.size, 0) / 1e6; if (total > this.maximumSize) { const sorted = files.sort((a, b) => a.stats.atimeMs - b.stats.atimeMs); let remain = total; for (const file of sorted) { if (remain <= this.maximumSize) break; fs_1.default.unlinkSync(file.path); remain -= file.stats.size; } } }; doit().catch(console.error); } } exports.Cache = Cache; exports.cache = new Cache();