@clickup/ent-framework
Version:
A PostgreSQL graph-database-alike library with microsharding and row-level security
152 lines • 5.78 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LocalCache = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = require("path");
const worker_threads_1 = require("worker_threads");
const defaults_1 = __importDefault(require("lodash/defaults"));
const misc_1 = require("../internal/misc");
/**
* A simple key-value cache stored on the local machine.
*
* - The expectation is that there will be not too many keys stored, since the
* background cleanup process running time to time is O(numKeysStored).
* - Guarantees corruption-free writes to the keys from multiple processes
* running concurrently.
* - The values which are not requested longer than approximately `expirationMs`
* are auto-removed.
* - Each key is stored in an individual file under `dir`. Some temporary files
* may also appear in that directory, but eventually, they will be cleaned up,
* even if they get stuck for some time.
*/
class LocalCache {
/**
* Initializes the instance.
*/
constructor(options) {
this.options = (0, defaults_1.default)({}, options, LocalCache.DEFAULT_OPTIONS);
this.cleanupTimeout = setTimeout(() => this.onCleanupTimer(), Math.round(this.options.cleanupFirstRunDelayMs *
(0, misc_1.jitter)(this.options.cleanupJitter))).unref();
}
/**
* Ends the instance lifecycle (e.g. garbage recheck interval).
*/
end() {
clearTimeout(this.cleanupTimeout);
this.cleanupTimeout = undefined;
}
/**
* Returns the value for the given key, or null if the key does not exist.
*/
async get(key) {
const path = this.buildPath(key);
try {
if (!fs_1.default.existsSync(path)) {
return null;
}
const data = await fs_1.default.promises.readFile(path, { encoding: "utf-8" });
return JSON.parse(data);
}
catch (e) {
rethrowExceptFileNotExistsError(e);
return null;
}
}
/**
* Sets the value for the given key.
*/
async set(key, value) {
const path = this.buildPath(key);
const data = JSON.stringify(value, undefined, 2);
try {
const stat = await fs_1.default.promises.stat(path);
const oldData = await fs_1.default.promises.readFile(path, { encoding: "utf-8" });
if (oldData === data) {
// Don't write if the data in the file is the same, just update mtime,
// but not too frequently too.
const now = Date.now();
if (stat.mtimeMs <
now -
this.options.expirationMs /
this.options.mtimeUpdatesOnReadPerExpiration) {
await fs_1.default.promises.utimes(path, now, now);
}
return;
}
}
catch (e) {
rethrowExceptFileNotExistsError(e);
}
await fs_1.default.promises.mkdir((0, path_1.dirname)(path), { recursive: true });
const tmpPath = this.buildPath(`${key}.${process.pid}-${worker_threads_1.threadId}.tmp`);
await fs_1.default.promises.writeFile(tmpPath, data);
await fs_1.default.promises.rename(tmpPath, path);
}
/**
* Deletes the values for keys which were not accessed for a long time.
*/
async cleanup() {
await fs_1.default.promises.mkdir(this.options.dir, { recursive: true });
const files = await fs_1.default.promises.readdir(this.options.dir);
const now = Date.now();
const dotExt = `.${this.options.ext}`;
for (const file of files) {
if (file.endsWith(dotExt)) {
const path = `${this.options.dir}/${file}`;
const stats = await fs_1.default.promises.stat(path);
if (now - stats.mtimeMs > this.options.expirationMs) {
try {
await fs_1.default.promises.unlink(path);
}
catch (e) {
rethrowExceptFileNotExistsError(e);
}
}
}
}
}
/**
* Runs then the instance creates (with initial small jitter) and also
* periodically.
*/
onCleanupTimer() {
this.cleanup().catch((error) => this.options.loggers.swallowedErrorLogger?.({
where: `${this.constructor.name}.cleanup`,
error,
elapsed: null,
importance: "normal",
}));
this.cleanupTimeout = setTimeout(() => this.onCleanupTimer(), Math.round((this.options.expirationMs / this.options.cleanupRoundsPerExpiration) *
(0, misc_1.jitter)(this.options.cleanupJitter))).unref();
}
/**
* Builds the full path to a file for a given key.
*/
buildPath(key) {
const filename = key.replace(/[^a-z0-9.]+/gi, "-");
return `${this.options.dir}/${filename}.${this.options.ext}`;
}
}
exports.LocalCache = LocalCache;
/** Default values for the constructor options. */
LocalCache.DEFAULT_OPTIONS = {
expirationMs: 1000 * 3600 * 24,
ext: "json",
cleanupJitter: 0.2,
cleanupFirstRunDelayMs: 30000,
cleanupRoundsPerExpiration: 2,
mtimeUpdatesOnReadPerExpiration: 10,
};
/**
* Throws the passed exception if it's a "file not found" error from Node fs
* module. Otherwise, does nothing.
*/
function rethrowExceptFileNotExistsError(e) {
if (e?.code !== "ENOENT") {
throw e;
}
}
//# sourceMappingURL=LocalCache.js.map
;