koatty_cacheable
Version:
Cacheable for koatty.
471 lines (462 loc) • 16.7 kB
JavaScript
/*!
* @Author: richen
* @Date: 2025-06-23 01:59:54
* @License: BSD (3-Clause)
* @Copyright (c) - <richenlin(at)gmail.com>
* @HomePage: https://koatty.org/
*/
;
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;