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
JavaScript
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