running prettier for cleanup
This commit is contained in:
parent
fe7733aeb5
commit
d85cd58acd
151 changed files with 29158 additions and 27966 deletions
42
libs/cache/src/connection-manager.ts
vendored
42
libs/cache/src/connection-manager.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
import Redis from 'ioredis';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { dragonflyConfig } from '@stock-bot/config';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
interface ConnectionConfig {
|
||||
name: string;
|
||||
|
|
@ -33,7 +33,7 @@ export class RedisConnectionManager {
|
|||
*/
|
||||
getConnection(config: ConnectionConfig): Redis {
|
||||
const { name, singleton = false, db } = config;
|
||||
|
||||
|
||||
if (singleton) {
|
||||
// Use shared connection across all instances
|
||||
if (!RedisConnectionManager.sharedConnections.has(name)) {
|
||||
|
|
@ -66,7 +66,9 @@ export class RedisConnectionManager {
|
|||
retryDelayOnFailover: dragonflyConfig.DRAGONFLY_RETRY_DELAY,
|
||||
connectTimeout: dragonflyConfig.DRAGONFLY_CONNECT_TIMEOUT,
|
||||
commandTimeout: dragonflyConfig.DRAGONFLY_COMMAND_TIMEOUT,
|
||||
keepAlive: dragonflyConfig.DRAGONFLY_ENABLE_KEEPALIVE ? dragonflyConfig.DRAGONFLY_KEEPALIVE_INTERVAL * 1000 : 0,
|
||||
keepAlive: dragonflyConfig.DRAGONFLY_ENABLE_KEEPALIVE
|
||||
? dragonflyConfig.DRAGONFLY_KEEPALIVE_INTERVAL * 1000
|
||||
: 0,
|
||||
connectionName: name,
|
||||
lazyConnect: false, // Connect immediately instead of waiting for first command
|
||||
...(dragonflyConfig.DRAGONFLY_TLS && {
|
||||
|
|
@ -90,7 +92,7 @@ export class RedisConnectionManager {
|
|||
this.logger.info(`Redis connection ready: ${name}`);
|
||||
});
|
||||
|
||||
redis.on('error', (err) => {
|
||||
redis.on('error', err => {
|
||||
this.logger.error(`Redis connection error for ${name}:`, err);
|
||||
});
|
||||
|
||||
|
|
@ -121,7 +123,7 @@ export class RedisConnectionManager {
|
|||
*/
|
||||
async closeAllConnections(): Promise<void> {
|
||||
// Close instance-specific connections
|
||||
const instancePromises = Array.from(this.connections.values()).map(conn =>
|
||||
const instancePromises = Array.from(this.connections.values()).map(conn =>
|
||||
this.closeConnection(conn)
|
||||
);
|
||||
await Promise.all(instancePromises);
|
||||
|
|
@ -129,8 +131,8 @@ export class RedisConnectionManager {
|
|||
|
||||
// Close shared connections (only if this is the last instance)
|
||||
if (RedisConnectionManager.instance === this) {
|
||||
const sharedPromises = Array.from(RedisConnectionManager.sharedConnections.values()).map(conn =>
|
||||
this.closeConnection(conn)
|
||||
const sharedPromises = Array.from(RedisConnectionManager.sharedConnections.values()).map(
|
||||
conn => this.closeConnection(conn)
|
||||
);
|
||||
await Promise.all(sharedPromises);
|
||||
RedisConnectionManager.sharedConnections.clear();
|
||||
|
|
@ -145,7 +147,7 @@ export class RedisConnectionManager {
|
|||
getConnectionCount(): { shared: number; unique: number } {
|
||||
return {
|
||||
shared: RedisConnectionManager.sharedConnections.size,
|
||||
unique: this.connections.size
|
||||
unique: this.connections.size,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -155,7 +157,7 @@ export class RedisConnectionManager {
|
|||
getConnectionNames(): { shared: string[]; unique: string[] } {
|
||||
return {
|
||||
shared: Array.from(RedisConnectionManager.sharedConnections.keys()),
|
||||
unique: Array.from(this.connections.keys())
|
||||
unique: Array.from(this.connections.keys()),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -198,10 +200,7 @@ export class RedisConnectionManager {
|
|||
*/
|
||||
static async waitForAllConnections(timeout: number = 30000): Promise<void> {
|
||||
const instance = this.getInstance();
|
||||
const allConnections = new Map([
|
||||
...instance.connections,
|
||||
...this.sharedConnections
|
||||
]);
|
||||
const allConnections = new Map([...instance.connections, ...this.sharedConnections]);
|
||||
|
||||
if (allConnections.size === 0) {
|
||||
instance.logger.info('No Redis connections to wait for');
|
||||
|
|
@ -210,7 +209,7 @@ export class RedisConnectionManager {
|
|||
|
||||
instance.logger.info(`Waiting for ${allConnections.size} Redis connections to be ready...`);
|
||||
|
||||
const connectionPromises = Array.from(allConnections.entries()).map(([name, redis]) =>
|
||||
const connectionPromises = Array.from(allConnections.entries()).map(([name, redis]) =>
|
||||
instance.waitForConnection(redis, name, timeout)
|
||||
);
|
||||
|
||||
|
|
@ -259,15 +258,12 @@ export class RedisConnectionManager {
|
|||
*/
|
||||
static areAllConnectionsReady(): boolean {
|
||||
const instance = this.getInstance();
|
||||
const allConnections = new Map([
|
||||
...instance.connections,
|
||||
...this.sharedConnections
|
||||
]);
|
||||
|
||||
return allConnections.size > 0 &&
|
||||
Array.from(allConnections.keys()).every(name =>
|
||||
this.readyConnections.has(name)
|
||||
);
|
||||
const allConnections = new Map([...instance.connections, ...this.sharedConnections]);
|
||||
|
||||
return (
|
||||
allConnections.size > 0 &&
|
||||
Array.from(allConnections.keys()).every(name => this.readyConnections.has(name))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
183
libs/cache/src/index.ts
vendored
183
libs/cache/src/index.ts
vendored
|
|
@ -1,92 +1,91 @@
|
|||
import { RedisCache } from './redis-cache';
|
||||
import { RedisConnectionManager } from './connection-manager';
|
||||
import type { CacheProvider, CacheOptions } from './types';
|
||||
|
||||
// Cache instances registry to prevent multiple instances with same prefix
|
||||
const cacheInstances = new Map<string, CacheProvider>();
|
||||
|
||||
/**
|
||||
* Create a Redis cache instance with trading-optimized defaults
|
||||
*/
|
||||
export function createCache(options: Partial<CacheOptions> = {}): CacheProvider {
|
||||
const defaultOptions: CacheOptions = {
|
||||
keyPrefix: 'cache:',
|
||||
ttl: 3600, // 1 hour default
|
||||
enableMetrics: true,
|
||||
shared: true, // Default to shared connections
|
||||
...options
|
||||
};
|
||||
|
||||
// For shared connections, reuse cache instances with the same key prefix
|
||||
if (defaultOptions.shared) {
|
||||
const cacheKey = `${defaultOptions.keyPrefix}-${defaultOptions.ttl}`;
|
||||
|
||||
if (cacheInstances.has(cacheKey)) {
|
||||
return cacheInstances.get(cacheKey)!;
|
||||
}
|
||||
|
||||
const cache = new RedisCache(defaultOptions);
|
||||
cacheInstances.set(cacheKey, cache);
|
||||
return cache;
|
||||
}
|
||||
|
||||
// For non-shared connections, always create new instances
|
||||
return new RedisCache(defaultOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cache instance for trading data
|
||||
*/
|
||||
export function createTradingCache(options: Partial<CacheOptions> = {}): CacheProvider {
|
||||
return createCache({
|
||||
keyPrefix: 'trading:',
|
||||
ttl: 3600, // 1 hour default
|
||||
enableMetrics: true,
|
||||
shared: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cache for market data with shorter TTL
|
||||
*/
|
||||
export function createMarketDataCache(options: Partial<CacheOptions> = {}): CacheProvider {
|
||||
return createCache({
|
||||
keyPrefix: 'market:',
|
||||
ttl: 300, // 5 minutes for market data
|
||||
enableMetrics: true,
|
||||
shared: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cache for indicators with longer TTL
|
||||
*/
|
||||
export function createIndicatorCache(options: Partial<CacheOptions> = {}): CacheProvider {
|
||||
return createCache({
|
||||
keyPrefix: 'indicators:',
|
||||
ttl: 1800, // 30 minutes for indicators
|
||||
enableMetrics: true,
|
||||
shared: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
// Export types and classes
|
||||
export type {
|
||||
CacheProvider,
|
||||
CacheOptions,
|
||||
CacheConfig,
|
||||
CacheStats,
|
||||
CacheKey,
|
||||
SerializationOptions
|
||||
} from './types';
|
||||
|
||||
export { RedisCache } from './redis-cache';
|
||||
export { RedisConnectionManager } from './connection-manager';
|
||||
export { CacheKeyGenerator } from './key-generator';
|
||||
|
||||
|
||||
// Default export for convenience
|
||||
export default createCache;
|
||||
import { RedisConnectionManager } from './connection-manager';
|
||||
import { RedisCache } from './redis-cache';
|
||||
import type { CacheOptions, CacheProvider } from './types';
|
||||
|
||||
// Cache instances registry to prevent multiple instances with same prefix
|
||||
const cacheInstances = new Map<string, CacheProvider>();
|
||||
|
||||
/**
|
||||
* Create a Redis cache instance with trading-optimized defaults
|
||||
*/
|
||||
export function createCache(options: Partial<CacheOptions> = {}): CacheProvider {
|
||||
const defaultOptions: CacheOptions = {
|
||||
keyPrefix: 'cache:',
|
||||
ttl: 3600, // 1 hour default
|
||||
enableMetrics: true,
|
||||
shared: true, // Default to shared connections
|
||||
...options,
|
||||
};
|
||||
|
||||
// For shared connections, reuse cache instances with the same key prefix
|
||||
if (defaultOptions.shared) {
|
||||
const cacheKey = `${defaultOptions.keyPrefix}-${defaultOptions.ttl}`;
|
||||
|
||||
if (cacheInstances.has(cacheKey)) {
|
||||
return cacheInstances.get(cacheKey)!;
|
||||
}
|
||||
|
||||
const cache = new RedisCache(defaultOptions);
|
||||
cacheInstances.set(cacheKey, cache);
|
||||
return cache;
|
||||
}
|
||||
|
||||
// For non-shared connections, always create new instances
|
||||
return new RedisCache(defaultOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cache instance for trading data
|
||||
*/
|
||||
export function createTradingCache(options: Partial<CacheOptions> = {}): CacheProvider {
|
||||
return createCache({
|
||||
keyPrefix: 'trading:',
|
||||
ttl: 3600, // 1 hour default
|
||||
enableMetrics: true,
|
||||
shared: true,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cache for market data with shorter TTL
|
||||
*/
|
||||
export function createMarketDataCache(options: Partial<CacheOptions> = {}): CacheProvider {
|
||||
return createCache({
|
||||
keyPrefix: 'market:',
|
||||
ttl: 300, // 5 minutes for market data
|
||||
enableMetrics: true,
|
||||
shared: true,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cache for indicators with longer TTL
|
||||
*/
|
||||
export function createIndicatorCache(options: Partial<CacheOptions> = {}): CacheProvider {
|
||||
return createCache({
|
||||
keyPrefix: 'indicators:',
|
||||
ttl: 1800, // 30 minutes for indicators
|
||||
enableMetrics: true,
|
||||
shared: true,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
// Export types and classes
|
||||
export type {
|
||||
CacheProvider,
|
||||
CacheOptions,
|
||||
CacheConfig,
|
||||
CacheStats,
|
||||
CacheKey,
|
||||
SerializationOptions,
|
||||
} from './types';
|
||||
|
||||
export { RedisCache } from './redis-cache';
|
||||
export { RedisConnectionManager } from './connection-manager';
|
||||
export { CacheKeyGenerator } from './key-generator';
|
||||
|
||||
// Default export for convenience
|
||||
export default createCache;
|
||||
|
|
|
|||
146
libs/cache/src/key-generator.ts
vendored
146
libs/cache/src/key-generator.ts
vendored
|
|
@ -1,73 +1,73 @@
|
|||
export class CacheKeyGenerator {
|
||||
/**
|
||||
* Generate cache key for market data
|
||||
*/
|
||||
static marketData(symbol: string, timeframe: string, date?: Date): string {
|
||||
const dateStr = date ? date.toISOString().split('T')[0] : 'latest';
|
||||
return `market:${symbol.toLowerCase()}:${timeframe}:${dateStr}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for technical indicators
|
||||
*/
|
||||
static indicator(symbol: string, indicator: string, period: number, dataHash: string): string {
|
||||
return `indicator:${symbol.toLowerCase()}:${indicator}:${period}:${dataHash}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for backtest results
|
||||
*/
|
||||
static backtest(strategyName: string, params: Record<string, any>): string {
|
||||
const paramHash = this.hashObject(params);
|
||||
return `backtest:${strategyName}:${paramHash}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for strategy results
|
||||
*/
|
||||
static strategy(strategyName: string, symbol: string, timeframe: string): string {
|
||||
return `strategy:${strategyName}:${symbol.toLowerCase()}:${timeframe}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for user sessions
|
||||
*/
|
||||
static userSession(userId: string): string {
|
||||
return `session:${userId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for portfolio data
|
||||
*/
|
||||
static portfolio(userId: string, portfolioId: string): string {
|
||||
return `portfolio:${userId}:${portfolioId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for real-time prices
|
||||
*/
|
||||
static realtimePrice(symbol: string): string {
|
||||
return `price:realtime:${symbol.toLowerCase()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for order book data
|
||||
*/
|
||||
static orderBook(symbol: string, depth: number = 10): string {
|
||||
return `orderbook:${symbol.toLowerCase()}:${depth}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a simple hash from object for cache keys
|
||||
*/
|
||||
private static hashObject(obj: Record<string, any>): string {
|
||||
const str = JSON.stringify(obj, Object.keys(obj).sort());
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
return Math.abs(hash).toString(36);
|
||||
}
|
||||
}
|
||||
export class CacheKeyGenerator {
|
||||
/**
|
||||
* Generate cache key for market data
|
||||
*/
|
||||
static marketData(symbol: string, timeframe: string, date?: Date): string {
|
||||
const dateStr = date ? date.toISOString().split('T')[0] : 'latest';
|
||||
return `market:${symbol.toLowerCase()}:${timeframe}:${dateStr}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for technical indicators
|
||||
*/
|
||||
static indicator(symbol: string, indicator: string, period: number, dataHash: string): string {
|
||||
return `indicator:${symbol.toLowerCase()}:${indicator}:${period}:${dataHash}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for backtest results
|
||||
*/
|
||||
static backtest(strategyName: string, params: Record<string, any>): string {
|
||||
const paramHash = this.hashObject(params);
|
||||
return `backtest:${strategyName}:${paramHash}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for strategy results
|
||||
*/
|
||||
static strategy(strategyName: string, symbol: string, timeframe: string): string {
|
||||
return `strategy:${strategyName}:${symbol.toLowerCase()}:${timeframe}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for user sessions
|
||||
*/
|
||||
static userSession(userId: string): string {
|
||||
return `session:${userId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for portfolio data
|
||||
*/
|
||||
static portfolio(userId: string, portfolioId: string): string {
|
||||
return `portfolio:${userId}:${portfolioId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for real-time prices
|
||||
*/
|
||||
static realtimePrice(symbol: string): string {
|
||||
return `price:realtime:${symbol.toLowerCase()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for order book data
|
||||
*/
|
||||
static orderBook(symbol: string, depth: number = 10): string {
|
||||
return `orderbook:${symbol.toLowerCase()}:${depth}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a simple hash from object for cache keys
|
||||
*/
|
||||
private static hashObject(obj: Record<string, any>): string {
|
||||
const str = JSON.stringify(obj, Object.keys(obj).sort());
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
return Math.abs(hash).toString(36);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
109
libs/cache/src/redis-cache.ts
vendored
109
libs/cache/src/redis-cache.ts
vendored
|
|
@ -1,7 +1,7 @@
|
|||
import Redis from 'ioredis';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { CacheProvider, CacheOptions, CacheStats } from './types';
|
||||
import { RedisConnectionManager } from './connection-manager';
|
||||
import { CacheOptions, CacheProvider, CacheStats } from './types';
|
||||
|
||||
/**
|
||||
* Simplified Redis-based cache provider using connection manager
|
||||
|
|
@ -15,27 +15,33 @@ export class RedisCache implements CacheProvider {
|
|||
private isConnected = false;
|
||||
private startTime = Date.now();
|
||||
private connectionManager: RedisConnectionManager;
|
||||
|
||||
|
||||
private stats: CacheStats = {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
errors: 0,
|
||||
hitRate: 0,
|
||||
total: 0,
|
||||
uptime: 0
|
||||
uptime: 0,
|
||||
};
|
||||
|
||||
constructor(options: CacheOptions = {}) {
|
||||
this.defaultTTL = options.ttl ?? 3600; // 1 hour default
|
||||
this.keyPrefix = options.keyPrefix ?? 'cache:';
|
||||
this.enableMetrics = options.enableMetrics ?? true;
|
||||
|
||||
|
||||
// Get connection manager instance
|
||||
this.connectionManager = RedisConnectionManager.getInstance();
|
||||
|
||||
|
||||
// Generate connection name based on cache type
|
||||
const baseName = options.name || this.keyPrefix.replace(':', '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase() || 'CACHE';
|
||||
|
||||
const baseName =
|
||||
options.name ||
|
||||
this.keyPrefix
|
||||
.replace(':', '')
|
||||
.replace(/[^a-zA-Z0-9]/g, '')
|
||||
.toUpperCase() ||
|
||||
'CACHE';
|
||||
|
||||
// Get Redis connection (shared by default for cache)
|
||||
this.redis = this.connectionManager.getConnection({
|
||||
name: `${baseName}-SERVICE`,
|
||||
|
|
@ -110,7 +116,7 @@ export class RedisCache implements CacheProvider {
|
|||
return await operation();
|
||||
} catch (error) {
|
||||
this.logger.error(`Redis ${operationName} failed`, {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
this.updateStats(false, true);
|
||||
return fallback;
|
||||
|
|
@ -122,7 +128,7 @@ export class RedisCache implements CacheProvider {
|
|||
async () => {
|
||||
const fullKey = this.getKey(key);
|
||||
const value = await this.redis.get(fullKey);
|
||||
|
||||
|
||||
if (value === null) {
|
||||
this.updateStats(false);
|
||||
this.logger.debug('Cache miss', { key });
|
||||
|
|
@ -131,7 +137,7 @@ export class RedisCache implements CacheProvider {
|
|||
|
||||
this.updateStats(true);
|
||||
this.logger.debug('Cache hit', { key });
|
||||
|
||||
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch {
|
||||
|
|
@ -144,23 +150,29 @@ export class RedisCache implements CacheProvider {
|
|||
);
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T, options?: number | {
|
||||
ttl?: number;
|
||||
preserveTTL?: boolean;
|
||||
onlyIfExists?: boolean;
|
||||
onlyIfNotExists?: boolean;
|
||||
getOldValue?: boolean;
|
||||
}): Promise<T | null> {
|
||||
async set<T>(
|
||||
key: string,
|
||||
value: T,
|
||||
options?:
|
||||
| number
|
||||
| {
|
||||
ttl?: number;
|
||||
preserveTTL?: boolean;
|
||||
onlyIfExists?: boolean;
|
||||
onlyIfNotExists?: boolean;
|
||||
getOldValue?: boolean;
|
||||
}
|
||||
): Promise<T | null> {
|
||||
return this.safeExecute(
|
||||
async () => {
|
||||
const fullKey = this.getKey(key);
|
||||
const serialized = typeof value === 'string' ? value : JSON.stringify(value);
|
||||
|
||||
|
||||
// Handle backward compatibility - if options is a number, treat as TTL
|
||||
const config = typeof options === 'number' ? { ttl: options } : (options || {});
|
||||
|
||||
const config = typeof options === 'number' ? { ttl: options } : options || {};
|
||||
|
||||
let oldValue: T | null = null;
|
||||
|
||||
|
||||
// Get old value if requested
|
||||
if (config.getOldValue) {
|
||||
const existingValue = await this.redis.get(fullKey);
|
||||
|
|
@ -172,15 +184,17 @@ export class RedisCache implements CacheProvider {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle preserveTTL logic
|
||||
if (config.preserveTTL) {
|
||||
const currentTTL = await this.redis.ttl(fullKey);
|
||||
|
||||
|
||||
if (currentTTL === -2) {
|
||||
// Key doesn't exist
|
||||
if (config.onlyIfExists) {
|
||||
this.logger.debug('Set skipped - key does not exist and onlyIfExists is true', { key });
|
||||
this.logger.debug('Set skipped - key does not exist and onlyIfExists is true', {
|
||||
key,
|
||||
});
|
||||
return oldValue;
|
||||
}
|
||||
// Set with default or specified TTL
|
||||
|
|
@ -201,7 +215,7 @@ export class RedisCache implements CacheProvider {
|
|||
if (config.onlyIfExists && config.onlyIfNotExists) {
|
||||
throw new Error('Cannot specify both onlyIfExists and onlyIfNotExists');
|
||||
}
|
||||
|
||||
|
||||
if (config.onlyIfExists) {
|
||||
// Only set if key exists (XX flag)
|
||||
const ttl = config.ttl ?? this.defaultTTL;
|
||||
|
|
@ -223,10 +237,10 @@ export class RedisCache implements CacheProvider {
|
|||
const ttl = config.ttl ?? this.defaultTTL;
|
||||
await this.redis.setex(fullKey, ttl, serialized);
|
||||
}
|
||||
|
||||
|
||||
this.logger.debug('Cache set', { key, ttl: config.ttl ?? this.defaultTTL });
|
||||
}
|
||||
|
||||
|
||||
return oldValue;
|
||||
},
|
||||
null,
|
||||
|
|
@ -278,8 +292,8 @@ export class RedisCache implements CacheProvider {
|
|||
const pong = await this.redis.ping();
|
||||
return pong === 'PONG';
|
||||
} catch (error) {
|
||||
this.logger.error('Redis health check failed', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
this.logger.error('Redis health check failed', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
|
@ -288,7 +302,7 @@ export class RedisCache implements CacheProvider {
|
|||
getStats(): CacheStats {
|
||||
return {
|
||||
...this.stats,
|
||||
uptime: Date.now() - this.startTime
|
||||
uptime: Date.now() - this.startTime,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -308,7 +322,7 @@ export class RedisCache implements CacheProvider {
|
|||
resolve();
|
||||
});
|
||||
|
||||
this.redis.once('error', (error) => {
|
||||
this.redis.once('error', error => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(error);
|
||||
});
|
||||
|
|
@ -318,12 +332,12 @@ export class RedisCache implements CacheProvider {
|
|||
isReady(): boolean {
|
||||
// Always check the actual Redis connection status
|
||||
const ready = this.redis.status === 'ready';
|
||||
|
||||
|
||||
// Update local flag if we're not using shared connection
|
||||
if (this.isConnected !== ready) {
|
||||
this.isConnected = ready;
|
||||
}
|
||||
|
||||
|
||||
return ready;
|
||||
}
|
||||
|
||||
|
|
@ -334,7 +348,7 @@ export class RedisCache implements CacheProvider {
|
|||
|
||||
async setIfExists<T>(key: string, value: T, ttl?: number): Promise<boolean> {
|
||||
const result = await this.set(key, value, { ttl, onlyIfExists: true });
|
||||
return result !== null || await this.exists(key);
|
||||
return result !== null || (await this.exists(key));
|
||||
}
|
||||
|
||||
async setIfNotExists<T>(key: string, value: T, ttl?: number): Promise<boolean> {
|
||||
|
|
@ -347,11 +361,15 @@ export class RedisCache implements CacheProvider {
|
|||
}
|
||||
|
||||
// Atomic update with transformation
|
||||
async updateField<T>(key: string, updater: (current: T | null) => T, ttl?: number): Promise<T | null> {
|
||||
async updateField<T>(
|
||||
key: string,
|
||||
updater: (current: T | null) => T,
|
||||
ttl?: number
|
||||
): Promise<T | null> {
|
||||
return this.safeExecute(
|
||||
async () => {
|
||||
const fullKey = this.getKey(key);
|
||||
|
||||
|
||||
// Use Lua script for atomic read-modify-write
|
||||
const luaScript = `
|
||||
local key = KEYS[1]
|
||||
|
|
@ -363,13 +381,12 @@ export class RedisCache implements CacheProvider {
|
|||
-- Return current value for processing
|
||||
return {current_value, current_ttl}
|
||||
`;
|
||||
|
||||
const [currentValue, currentTTL] = await this.redis.eval(
|
||||
luaScript,
|
||||
1,
|
||||
fullKey
|
||||
) as [string | null, number];
|
||||
|
||||
|
||||
const [currentValue, currentTTL] = (await this.redis.eval(luaScript, 1, fullKey)) as [
|
||||
string | null,
|
||||
number,
|
||||
];
|
||||
|
||||
// Parse current value
|
||||
let parsed: T | null = null;
|
||||
if (currentValue !== null) {
|
||||
|
|
@ -379,10 +396,10 @@ export class RedisCache implements CacheProvider {
|
|||
parsed = currentValue as unknown as T;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Apply updater function
|
||||
const newValue = updater(parsed);
|
||||
|
||||
|
||||
// Set the new value with appropriate TTL logic
|
||||
if (ttl !== undefined) {
|
||||
// Use specified TTL
|
||||
|
|
@ -394,7 +411,7 @@ export class RedisCache implements CacheProvider {
|
|||
// Preserve existing TTL
|
||||
await this.set(key, newValue, { preserveTTL: true });
|
||||
}
|
||||
|
||||
|
||||
return parsed;
|
||||
},
|
||||
null,
|
||||
|
|
|
|||
174
libs/cache/src/types.ts
vendored
174
libs/cache/src/types.ts
vendored
|
|
@ -1,84 +1,90 @@
|
|||
export interface CacheProvider {
|
||||
get<T>(key: string): Promise<T | null>;
|
||||
set<T>(key: string, value: T, options?: number | {
|
||||
ttl?: number;
|
||||
preserveTTL?: boolean;
|
||||
onlyIfExists?: boolean;
|
||||
onlyIfNotExists?: boolean;
|
||||
getOldValue?: boolean;
|
||||
}): Promise<T | null>;
|
||||
del(key: string): Promise<void>;
|
||||
exists(key: string): Promise<boolean>;
|
||||
clear(): Promise<void>;
|
||||
getStats(): CacheStats;
|
||||
health(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Wait for the cache to be ready and connected
|
||||
* @param timeout Maximum time to wait in milliseconds (default: 5000)
|
||||
* @returns Promise that resolves when cache is ready
|
||||
*/
|
||||
waitForReady(timeout?: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* Check if the cache is currently ready
|
||||
*/
|
||||
isReady(): boolean;
|
||||
|
||||
// Enhanced cache methods
|
||||
/**
|
||||
* Update value preserving existing TTL
|
||||
*/
|
||||
update?<T>(key: string, value: T): Promise<T | null>;
|
||||
|
||||
/**
|
||||
* Set value only if key exists
|
||||
*/
|
||||
setIfExists?<T>(key: string, value: T, ttl?: number): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Set value only if key doesn't exist
|
||||
*/
|
||||
setIfNotExists?<T>(key: string, value: T, ttl?: number): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Replace existing key's value and TTL
|
||||
*/
|
||||
replace?<T>(key: string, value: T, ttl?: number): Promise<T | null>;
|
||||
/**
|
||||
* Atomically update field with transformation function
|
||||
*/
|
||||
updateField?<T>(key: string, updater: (current: T | null) => T, ttl?: number): Promise<T | null>;
|
||||
}
|
||||
|
||||
export interface CacheOptions {
|
||||
ttl?: number;
|
||||
keyPrefix?: string;
|
||||
enableMetrics?: boolean;
|
||||
name?: string; // Name for connection identification
|
||||
shared?: boolean; // Whether to use shared connection
|
||||
}
|
||||
|
||||
export interface CacheStats {
|
||||
hits: number;
|
||||
misses: number;
|
||||
errors: number;
|
||||
hitRate: number;
|
||||
total: number;
|
||||
uptime: number;
|
||||
}
|
||||
|
||||
export interface CacheConfig {
|
||||
type: 'redis';
|
||||
keyPrefix?: string;
|
||||
defaultTTL?: number;
|
||||
enableMetrics?: boolean;
|
||||
compression?: boolean;
|
||||
}
|
||||
|
||||
export type CacheKey = string | (() => string);
|
||||
|
||||
export interface SerializationOptions {
|
||||
compress?: boolean;
|
||||
binary?: boolean;
|
||||
}
|
||||
export interface CacheProvider {
|
||||
get<T>(key: string): Promise<T | null>;
|
||||
set<T>(
|
||||
key: string,
|
||||
value: T,
|
||||
options?:
|
||||
| number
|
||||
| {
|
||||
ttl?: number;
|
||||
preserveTTL?: boolean;
|
||||
onlyIfExists?: boolean;
|
||||
onlyIfNotExists?: boolean;
|
||||
getOldValue?: boolean;
|
||||
}
|
||||
): Promise<T | null>;
|
||||
del(key: string): Promise<void>;
|
||||
exists(key: string): Promise<boolean>;
|
||||
clear(): Promise<void>;
|
||||
getStats(): CacheStats;
|
||||
health(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Wait for the cache to be ready and connected
|
||||
* @param timeout Maximum time to wait in milliseconds (default: 5000)
|
||||
* @returns Promise that resolves when cache is ready
|
||||
*/
|
||||
waitForReady(timeout?: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* Check if the cache is currently ready
|
||||
*/
|
||||
isReady(): boolean;
|
||||
|
||||
// Enhanced cache methods
|
||||
/**
|
||||
* Update value preserving existing TTL
|
||||
*/
|
||||
update?<T>(key: string, value: T): Promise<T | null>;
|
||||
|
||||
/**
|
||||
* Set value only if key exists
|
||||
*/
|
||||
setIfExists?<T>(key: string, value: T, ttl?: number): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Set value only if key doesn't exist
|
||||
*/
|
||||
setIfNotExists?<T>(key: string, value: T, ttl?: number): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Replace existing key's value and TTL
|
||||
*/
|
||||
replace?<T>(key: string, value: T, ttl?: number): Promise<T | null>;
|
||||
/**
|
||||
* Atomically update field with transformation function
|
||||
*/
|
||||
updateField?<T>(key: string, updater: (current: T | null) => T, ttl?: number): Promise<T | null>;
|
||||
}
|
||||
|
||||
export interface CacheOptions {
|
||||
ttl?: number;
|
||||
keyPrefix?: string;
|
||||
enableMetrics?: boolean;
|
||||
name?: string; // Name for connection identification
|
||||
shared?: boolean; // Whether to use shared connection
|
||||
}
|
||||
|
||||
export interface CacheStats {
|
||||
hits: number;
|
||||
misses: number;
|
||||
errors: number;
|
||||
hitRate: number;
|
||||
total: number;
|
||||
uptime: number;
|
||||
}
|
||||
|
||||
export interface CacheConfig {
|
||||
type: 'redis';
|
||||
keyPrefix?: string;
|
||||
defaultTTL?: number;
|
||||
enableMetrics?: boolean;
|
||||
compression?: boolean;
|
||||
}
|
||||
|
||||
export type CacheKey = string | (() => string);
|
||||
|
||||
export interface SerializationOptions {
|
||||
compress?: boolean;
|
||||
binary?: boolean;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue