UNPKG

bentocache

Version:

Multi-tier cache module for Node.js. Redis, Upstash, CloudfareKV, File, in-memory and others drivers

199 lines (198 loc) 5.59 kB
import { Locks } from "../../../chunk-TLJ7G3ZX.js"; import { resolveTtl } from "../../../chunk-YAGCWAYQ.js"; import { BaseDriver } from "../../../chunk-BO75WXSS.js"; // src/drivers/file/file.ts import { dirname, join } from "path"; import { Worker } from "worker_threads"; import { access, mkdir, readFile, writeFile, rm } from "fs/promises"; function fileDriver(options) { return { options, factory: (config) => new FileDriver(config) }; } var FileDriver = class _FileDriver extends BaseDriver { type = "l2"; /** * Root directory for storing the cache files */ #directory; /** * Worker thread that will clean up the expired files */ #cleanerWorker; #locks = new Locks(); constructor(config, isNamespace = false) { super(config); this.#directory = this.#sanitizePath(join(config.directory, config.prefix || "")); if (isNamespace) return; if (config.pruneInterval === false) return; this.#cleanerWorker = new Worker(new URL("./cleaner_worker.js", import.meta.url), { workerData: { directory: this.#directory, pruneInterval: resolveTtl(config.pruneInterval) } }); const logger = this.config.logger?.child({ context: "bentocache.file-driver" }); this.#cleanerWorker.on("message", (message) => { if (message.type === "error") { ; (logger || console).error({ err: message.error }, "failed to prune expired items"); } else if (message.type === "info") { logger?.info(message.message); } }); } /** * A simple mutex to write to the file system to avoid any * compromised data */ #runExclusiveKey(key, fn) { const lock = this.#locks.getOrCreateForKey(key); return lock.runExclusive(fn); } /** * Since keys and namespace uses `:` as a separator, we need to * purge them from the given path. We replace them with `/` to * create a nested directory structure. */ #sanitizePath(path) { if (!path) return ""; return path.replaceAll(":", "/"); } /** * Converts the given key to a file path */ #keyToPath(key) { const keyWithoutPrefix = key.replace(this.prefix, ""); const re = /(\.\/|\.\.\/)/g; if (re.test(key)) { throw new Error(`Invalid key: ${keyWithoutPrefix}. Should not contain relative paths.`); } return join(this.#directory, this.#sanitizePath(keyWithoutPrefix)); } /** * Check if a file exists at a given path or not */ async #pathExists(path) { try { await access(path); return true; } catch { return false; } } /** * Output a file to the disk and create the directory recursively if * it's missing */ async #outputFile(filename, content) { const directory = dirname(filename); const pathExists = await this.#pathExists(directory); if (!pathExists) { await mkdir(directory, { recursive: true }); } await writeFile(filename, content); } /** * Returns a new instance of the driver namespaced */ namespace(namespace) { return new _FileDriver({ ...this.config, prefix: this.createNamespacePrefix(namespace) }, true); } /** * Get a value from the cache */ async get(key) { key = this.getItemKey(key); const path = this.#keyToPath(key); const pathExists = await this.#pathExists(path); if (!pathExists) return void 0; const content = await readFile(path, { encoding: "utf-8" }); const [value, expire] = JSON.parse(content); if (expire !== -1 && expire < Date.now()) { await this.delete(key); return void 0; } return value; } /** * Get the value of a key and delete it * * Returns the value if the key exists, undefined otherwise */ async pull(key) { const value = await this.get(key); if (!value) return void 0; await this.delete(key); return value; } /** * Put a value in the cache * Returns true if the value was set, false otherwise */ async set(key, value, ttl) { return this.#runExclusiveKey(key, async () => { key = this.getItemKey(key); await this.#outputFile( this.#keyToPath(key), JSON.stringify([value, ttl ? Date.now() + ttl : -1]) ); return true; }); } /** * Remove all items from the cache */ async clear() { const cacheExists = await this.#pathExists(this.#directory); if (!cacheExists) return; await rm(this.#directory, { recursive: true }); } /** * Delete a key from the cache * Returns true if the key was deleted, false otherwise */ async delete(key) { key = this.getItemKey(key); const path = this.#keyToPath(key); const pathExists = await this.#pathExists(path); if (!pathExists) { return false; } await rm(path); return true; } /** * Delete multiple keys from the cache */ async deleteMany(keys) { if (keys.length === 0) return true; await Promise.all(keys.map((key) => this.delete(key))); return true; } async disconnect() { await this.#cleanerWorker?.terminate(); } /** * Manually prune expired cache entries by scanning the cache directory * and removing files that have expired. */ async prune() { const cacheExists = await this.#pathExists(this.#directory); if (!cacheExists) return; const { pruneExpiredFiles } = await import("../../../cleaner-HDROJ5PH.js"); await pruneExpiredFiles({ directory: this.#directory, onError: (err) => this.config.logger?.error(err) }); } }; export { FileDriver, fileDriver }; //# sourceMappingURL=file.js.map