running prettier for cleanup

This commit is contained in:
Boki 2025-06-11 10:13:25 -04:00
parent fe7733aeb5
commit d85cd58acd
151 changed files with 29158 additions and 27966 deletions

View file

@ -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))
);
}
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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,

View file

@ -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;
}