UNPKG

koatty_cacheable

Version:
471 lines (462 loc) 16.7 kB
/*! * @Author: richen * @Date: 2025-06-23 01:59:54 * @License: BSD (3-Clause) * @Copyright (c) - <richenlin(at)gmail.com> * @HomePage: https://koatty.org/ */ 'use strict'; var koatty_logger = require('koatty_logger'); var koatty_lib = require('koatty_lib'); var koatty_store = require('koatty_store'); var koatty_container = require('koatty_container'); // storeCache const storeCache = { store: null }; /** * get instances of storeCache * * @export * @param {Application} app * @returns {*} {CacheStore} */ async function GetCacheStore(app) { if (storeCache.store && storeCache.store.getConnection) { return storeCache.store; } let opt = { type: "memory", db: 0, timeout: 30 }; if (app && koatty_lib.Helper.isFunction(app.config)) { opt = app.config("CacheStore") || app.config("CacheStore", "db"); if (koatty_lib.Helper.isEmpty(opt)) { koatty_logger.DefaultLogger.Warn(`Missing CacheStore server configuration. Please write a configuration item with the key name 'CacheStore' in the db.ts file.`); } } storeCache.store = koatty_store.CacheStore.getInstance(opt); if (!koatty_lib.Helper.isFunction(storeCache.store.getConnection)) { throw Error(`CacheStore connection failed. `); } await storeCache.store.client.getConnection(); return storeCache.store; } /* * @Author: richen * @Date: 2020-07-06 19:53:43 * @LastEditTime: 2025-06-23 15:53:46 * @Description: * @Copyright (c) - <richenlin(at)gmail.com> */ class CacheManager { static instance; cacheStore = null; defaultTimeout = 300; defaultDelayedDoubleDeletion = true; static getInstance() { if (!CacheManager.instance) { CacheManager.instance = new CacheManager(); } return CacheManager.instance; } setCacheStore(store) { this.cacheStore = store; } getCacheStore() { return this.cacheStore; } setDefaultConfig(timeout, delayedDoubleDeletion) { if (timeout !== undefined) this.defaultTimeout = timeout; if (delayedDoubleDeletion !== undefined) this.defaultDelayedDoubleDeletion = delayedDoubleDeletion; } getDefaultTimeout() { return this.defaultTimeout; } getDefaultDelayedDoubleDeletion() { return this.defaultDelayedDoubleDeletion; } } /** * @Description: Cache injector, unified processing of all cache decorators * @Usage: * @Author: richen * @Date: 2025-01-10 14:00:00 * @LastEditTime: 2025-01-10 14:00:00 * @License: BSD (3-Clause) * @Copyright (c): <richenlin(at)gmail.com> */ // import { Helper } from 'koatty_lib'; // Create logger instance const logger$1 = new koatty_logger.Logger(); /** * Cache injector - initialize global cache manager and store * @param options Cache options * @param app Koatty application instance */ async function injectCache(options, app) { try { logger$1.Debug('Initializing cache system...'); // Get cache store instance const store = await GetCacheStore(app); if (!store) { logger$1.Warn('Cache store unavailable, cache system disabled'); return; } // Initialize global cache manager const cacheManager = CacheManager.getInstance(); cacheManager.setCacheStore(store); // Set default configuration cacheManager.setDefaultConfig(options.cacheTimeout || 300, options.delayedDoubleDeletion !== undefined ? options.delayedDoubleDeletion : true); logger$1.Info(`Cache system initialized successfully with timeout: ${options.cacheTimeout || 300}s`); } catch (error) { logger$1.Error('Cache system initialization failed:', error); } } /** * Close cache store connection * @param app Koatty application instance */ async function closeCacheStore(_app) { try { logger$1.Debug('Closing cache store connection...'); // Reset global cache manager const cacheManager = CacheManager.getInstance(); const store = cacheManager.getCacheStore(); await store?.close(); cacheManager.setCacheStore(null); logger$1.Info('Cache store connection closed'); } catch (error) { logger$1.Error('Error closing cache store connection:', error); } } /* eslint-disable @typescript-eslint/no-unused-vars */ /* * @Description: * @Usage: * @Author: richen * @Date: 2024-11-07 13:54:24 * @LastEditTime: 2024-11-07 15:25:36 * @License: BSD (3-Clause) * @Copyright (c): <richenlin(at)gmail.com> */ const longKey = 128; /** * Extract parameter names from function signature * @param func The function to extract parameters from * @returns Array of parameter names */ function getArgs(func) { try { // Match function parameters in parentheses const args = func.toString().match(/.*?\(([^)]*)\)/); if (args && args.length > 1) { // Split parameters into array and clean them return args[1].split(",").map(function (a) { // Remove inline comments and whitespace return a.replace(/\/\*.*\*\//, "").trim(); }).filter(function (ae) { // Filter out empty strings return ae; }); } return []; } catch (error) { // Return empty array if parsing fails return []; } } /** * Get parameter indexes based on parameter names * @param funcParams Function parameter names * @param params Target parameter names to find indexes for * @returns Array of parameter indexes (-1 if not found) */ function getParamIndex(funcParams, params) { return params.map(param => funcParams.indexOf(param)); } /** * Generate cache key based on cache name and parameters * @param cacheName base cache name * @param paramIndexes parameter indexes * @param paramNames parameter names * @param props method arguments * @returns generated cache key */ function generateCacheKey(cacheName, paramIndexes, paramNames, props) { let key = cacheName; for (let i = 0; i < paramIndexes.length; i++) { const paramIndex = paramIndexes[i]; if (paramIndex >= 0 && props[paramIndex] !== undefined) { key += `:${paramNames[i]}:${koatty_lib.Helper.toString(props[paramIndex])}`; } } return key.length > longKey ? koatty_lib.Helper.murmurHash(key) : key; } /** * Create a delay promise * @param ms Delay time in milliseconds * @returns Promise that resolves after the specified delay */ function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Execute a function after a specified delay * @param fn Function to execute * @param ms Delay time in milliseconds * @returns Promise that resolves with the function result */ async function asyncDelayedExecution(fn, ms) { await delay(ms); return fn(); } /* * @Author: richen * @Date: 2020-07-06 19:53:43 * @LastEditTime: 2024-11-07 15:53:46 * @Description: * @Copyright (c) - <richenlin(at)gmail.com> */ // Create logger instance const logger = new koatty_logger.Logger(); // Define cache decorator types exports.DecoratorType = void 0; (function (DecoratorType) { DecoratorType["CACHE_EVICT"] = "CACHE_EVICT"; DecoratorType["CACHE_ABLE"] = "CACHE_ABLE"; })(exports.DecoratorType || (exports.DecoratorType = {})); // IOC container key constant const COMPONENT_CACHE = "COMPONENT_CACHE"; const CACHE_METADATA_KEY = "CACHE_METADATA_KEY"; /** * Decorate this method to support caching. * The cache method returns a value to ensure that the next time * the method is executed with the same parameters, the results can be obtained * directly from the cache without the need to execute the method again. * CacheStore server config defined in db.ts. * * @export * @param {string} cacheName cache name * @param {CacheAbleOpt} [opt] cache options * e.g: * { * params: ["id"], * timeout: 30 * } * Use the 'id' parameters of the method as cache subkeys, the cache expiration time 30s * @returns {MethodDecorator} */ function CacheAble(cacheNameOrOpt, opt = {}) { // Handle overloaded parameters let cacheName; let options; if (typeof cacheNameOrOpt === 'string') { cacheName = cacheNameOrOpt; options = opt; } else { options = cacheNameOrOpt || {}; cacheName = options.cacheName; } return (target, methodName, descriptor) => { const componentType = koatty_container.IOCContainer.getType(target); if (!["SERVICE", "COMPONENT"].includes(componentType)) { throw Error("This decorator only used in the service、component class."); } // Generate cache name if not provided const finalCacheName = cacheName || `${target.constructor.name}:${String(methodName)}`; // Get original method const originalMethod = descriptor.value; if (!koatty_lib.Helper.isFunction(originalMethod)) { throw new Error(`CacheAble decorator can only be applied to methods`); } // Create wrapped method descriptor.value = function (...args) { const cacheManager = CacheManager.getInstance(); const store = cacheManager.getCacheStore(); // If cache store is not available, execute original method directly if (!store) { logger.Debug(`Cache store not available for ${finalCacheName}, executing original method`); return originalMethod.apply(this, args); } // Get method parameter list const funcParams = getArgs(originalMethod); // Get cache parameter positions const paramIndexes = getParamIndex(funcParams, options.params || []); return (async () => { try { // Generate cache key const key = generateCacheKey(finalCacheName, paramIndexes, options.params || [], args); // Try to get data from cache const cached = await store.get(key).catch((e) => { logger.Debug("Cache get error:" + e.message); return null; }); if (!koatty_lib.Helper.isEmpty(cached)) { logger.Debug(`Cache hit for key: ${key}`); try { return JSON.parse(cached); } catch { // If parse fails, return as string (for simple values) return cached; } } logger.Debug(`Cache miss for key: ${key}`); // Execute original method const result = await originalMethod.apply(this, args); // Use decorator timeout if specified, otherwise use global default const timeout = options.timeout || cacheManager.getDefaultTimeout(); // Asynchronously set cache store.set(key, koatty_lib.Helper.isJSONObj(result) ? JSON.stringify(result) : result, timeout).catch((e) => { logger.Debug("Cache set error:" + e.message); }); return result; } catch (error) { logger.Debug(`CacheAble wrapper error: ${error.message}`); // If cache operation fails, execute original method directly return originalMethod.apply(this, args); } })(); }; return descriptor; }; } /** * Decorating the execution of this method will trigger a cache clear operation. * CacheStore server config defined in db.ts. * * @export * @param {string} cacheName cacheName cache name * @param {CacheEvictOpt} [opt] cache options * e.g: * { * params: ["id"], * delayedDoubleDeletion: true * } * Use the 'id' parameters of the method as cache subkeys, * and clear the cache after the method executed * @returns */ function CacheEvict(cacheNameOrOpt, opt = {}) { // Handle overloaded parameters let cacheName; let options; if (typeof cacheNameOrOpt === 'string') { cacheName = cacheNameOrOpt; options = opt; } else { options = cacheNameOrOpt || {}; cacheName = options.cacheName; } return (target, methodName, descriptor) => { const componentType = koatty_container.IOCContainer.getType(target); if (!["SERVICE", "COMPONENT"].includes(componentType)) { throw Error("This decorator only used in the service、component class."); } // Save class to IOC container for tracking koatty_container.IOCContainer.saveClass("COMPONENT", target, COMPONENT_CACHE); // Generate cache name if not provided const finalCacheName = cacheName || `${target.constructor.name}:${String(methodName)}`; // Get original method const originalMethod = descriptor.value; if (!koatty_lib.Helper.isFunction(originalMethod)) { throw new Error(`CacheEvict decorator can only be applied to methods`); } // Create wrapped method descriptor.value = function (...args) { const cacheManager = CacheManager.getInstance(); const store = cacheManager.getCacheStore(); // If cache store is not available, execute original method directly if (!store) { logger.Debug(`Cache store not available for ${finalCacheName}, executing original method`); return originalMethod.apply(this, args); } // Get method parameter list const funcParams = getArgs(originalMethod); // Get cache parameter positions const paramIndexes = getParamIndex(funcParams, options.params || []); return (async () => { try { // Generate cache key const key = generateCacheKey(finalCacheName, paramIndexes, options.params || [], args); // Execute original method const result = await originalMethod.apply(this, args); // Immediately clear cache store.del(key).catch((e) => { logger.Debug("Cache delete error:" + e.message); }); // Use decorator setting if specified, otherwise use global default const enableDelayedDeletion = options.delayedDoubleDeletion !== undefined ? options.delayedDoubleDeletion : cacheManager.getDefaultDelayedDoubleDeletion(); // Delayed double deletion strategy if (enableDelayedDeletion !== false) { const delayTime = 5000; asyncDelayedExecution(() => { store.del(key).catch((e) => { logger.Debug("Cache double delete error:" + e.message); }); }, delayTime); } return result; } catch (error) { logger.Debug(`CacheEvict wrapper error: ${error.message}`); // If cache operation fails, execute original method directly return originalMethod.apply(this, args); } })(); }; return descriptor; }; } /* * @Description: * @Usage: * @Author: richen * @Date: 2024-11-07 16:00:02 * @LastEditTime: 2024-11-07 16:00:05 * @License: BSD (3-Clause) * @Copyright (c): <richenlin(at)gmail.com> */ /** * defaultOptions */ const defaultOptions = { cacheTimeout: 300, delayedDoubleDeletion: true, redisConfig: { host: "localhost", port: 6379, password: "", db: 0, keyPrefix: "redlock:" } }; /** * @param options - The options for the scheduled job * @param app - The Koatty application instance */ async function KoattyCache(options, app) { options = { ...defaultOptions, ...options }; // inject cache decorator await injectCache(options, app); // Register cleanup on app stop app.on('appStop', async () => { await closeCacheStore(); }); } exports.CACHE_METADATA_KEY = CACHE_METADATA_KEY; exports.CacheAble = CacheAble; exports.CacheEvict = CacheEvict; exports.KoattyCache = KoattyCache;