UNPKG

@clickup/ent-framework

Version:

A PostgreSQL graph-database-alike library with microsharding and row-level security

152 lines 5.78 kB
"use strict"; 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