UNPKG

@chrono-cache/core

Version:

Backbone of chrono-cache, designed to provide a flexible and efficient approach to cache management

276 lines (271 loc) 7.19 kB
// source/utils/console.ts var consoleColors = { text: { red: (val) => `\x1B[31m${val}\x1B[0m`, green: (val) => `\x1B[32m${val}\x1B[0m`, yellow: (val) => `\x1B[33m${val}\x1B[0m`, blue: (val) => `\x1B[34m${val}\x1B[0m` }, background: { red: (val) => `\x1B[41m${val}\x1B[0m`, green: (val) => `\x1B[42m${val}\x1B[0m`, yellow: (val) => `\x1B[43m${val}\x1B[0m`, blue: (val) => `\x1B[44m${val}\x1B[0m` } }; // source/cache-modules/LRUCache.ts var LRUCache = class { memoryCache = /* @__PURE__ */ new Map(); cacheSizes = /* @__PURE__ */ new Map(); serializeValue; maxSize = 10 * 1024 * 1024; totalSize = 0; ttl = 6e4; debug = false; constructor(options) { this.debug = options.debug ?? false; this.serializeValue = options.serializeValue; if (options.maxSize) this.maxSize = options.maxSize; if (options.ttl) { this.ttl = options.ttl; } } /** * Get memory cache value */ get(key) { const memoryValue = this.memoryCache.get(key); if (!memoryValue || memoryValue.expiresAt <= Date.now()) { if (this.debug) console.log( consoleColors.background.green("[CACHE:MEMORY]"), consoleColors.text.yellow("[SKIP]") ); return null; } if (this.debug) { console.log( consoleColors.background.green("[CACHE:MEMORY]"), consoleColors.text.green("[HIT]") ); } this.touch(key); return memoryValue; } /** * Set memory value */ set(key, value, tags = []) { const size = this.calculateSize(value); if (size > this.maxSize) { console.log( consoleColors.background.green("[CACHE:MEMORY]"), consoleColors.text.red("Single item size exceeds max size") ); return; } if (this.has(key)) { this.totalSize -= this.cacheSizes.get(key) || 0; } this.cacheSizes.set(key, size); this.memoryCache.set(key, { value, tags, lastModified: Date.now(), expiresAt: Date.now() + this.ttl }); this.totalSize += size; this.touch(key); } has(key) { if (!key) return false; this.touch(key); return Boolean(this.memoryCache.get(key)); } touch(key) { const value = this.memoryCache.get(key); if (value) { this.memoryCache.delete(key); this.memoryCache.set(key, value); this.evictIfNecessary(); } } evictIfNecessary() { while (this.totalSize > this.maxSize && this.memoryCache.size > 0) { this.evictLeastRecentlyUsed(); } } evictLeastRecentlyUsed() { const lruKey = this.getOldestCacheKey(); if (lruKey !== void 0) { const lruSize = this.cacheSizes.get(lruKey) || 0; this.totalSize -= lruSize; this.memoryCache.delete(lruKey); this.cacheSizes.delete(lruKey); } } getOldestCacheKey() { return this.memoryCache.keys().next().value; } calculateSize(value) { return Buffer.byteLength(this.serializeValue(value), "utf8"); } /** * Revalidate using tags array */ revalidateTags(tags) { this.memoryCache.forEach((value, key) => { if (tags.some((tag) => value.tags?.includes(tag))) { this.memoryCache.delete(key); } }); } }; // source/cache-modules/FileCache/index.ts import path2 from "node:path"; // source/cache-modules/FileCache/ManifestManager.ts import path from "node:path"; var ManifestManager = class _ManifestManager { static fileName = "tags-manifest.json"; value; baseTags = []; fs; dir; constructor(options) { this.dir = options.dir; this.fs = options.fs; if (options.baseTags?.length) this.baseTags.push(...options.baseTags); } get filePath() { return path.join(this.dir, _ManifestManager.fileName); } isLoaded() { return !!this.value; } async updateRevalidatedAt(tags) { for (const tag of tags) { const data = this.value?.items[tag] || { revalidatedAt: Date.now() }; data.revalidatedAt = Date.now(); if (this.value) { this.value.items[tag] = data; } } await this.write(); } async write() { try { await this.fs.mkdir(path.dirname(this.dir)); await this.fs.writeFile(this.filePath, JSON.stringify(this?.value || {})); } catch (err) { } } /** * Load the tags manifest from the file system */ async load() { try { this.value = JSON.parse(await this.fs.readFile(this.filePath, "utf8")); } catch (err) { this.value = { version: 1, items: {} }; } } /** * Check if tag is expired */ expired(tag, lastModified) { const now = Date.now(); return this.baseTags.includes(tag) || this.value?.items[tag]?.revalidatedAt && this.value?.items[tag].revalidatedAt >= lastModified; } }; // source/cache-modules/FileCache/index.ts var FileCache = class { distDir; debug = false; fs; manifestManager; constructor(options) { this.fs = options.fs; this.debug = !!options.debug; this.distDir = options.dir; this.manifestManager = new ManifestManager({ dir: options.dir, fs: options.fs, baseTags: [] }); } /** * Get cached value */ async get(key) { try { const filePath = this.getFilePath([key]); const fileTagsPath = this.getFilePath(["tags", key]); const [fileData, fileTags] = await Promise.all([ this.fs.readFile(filePath, "utf8"), this.fs.readFile(fileTagsPath, "utf8") ]); const { mtime } = await this.fs.stat(filePath); const tags = JSON.parse(fileTags); if (!fileData || await this.isExpired(tags, mtime.getTime())) { throw new Error("Not found or expired"); } if (this.debug) console.log( consoleColors.background.blue("[CACHE:SYSTEM]"), consoleColors.text.green("[HIT]") ); return { lastModified: mtime.getTime(), value: fileData, tags }; } catch (error) { } if (this.debug) console.log( consoleColors.background.blue("[CACHE:SYSTEM]"), consoleColors.text.red("[SKIP]") ); return await Promise.resolve(null); } /** * set cache */ async set(key, data, tags = []) { const filePath = this.getFilePath([key]); const fileTagsPath = this.getFilePath(["tags", key]); await Promise.all([ this.fs.mkdir(path2.dirname(filePath)), this.fs.mkdir(path2.dirname(fileTagsPath)) ]); await Promise.all([ await this.fs.writeFile(filePath, data), await this.fs.writeFile(fileTagsPath, JSON.stringify(tags)) ]); } /** * Revalidate cache using tags */ async revalidateTags(tags) { await this.manifestManager.load(); if (this.manifestManager.isLoaded()) { await this.manifestManager.updateRevalidatedAt(tags); } } async isExpired(tags, lastModified) { await this.manifestManager.load(); const wasRevalidated = tags.some((tag) => { return this.manifestManager.expired(tag, lastModified); }); return wasRevalidated; } getFilePath(pathname) { return path2.join(this.distDir, ...pathname); } }; export { FileCache, LRUCache };