diff --git a/libs/cache/package.json b/libs/cache/package.json new file mode 100644 index 0000000..098c126 --- /dev/null +++ b/libs/cache/package.json @@ -0,0 +1,21 @@ +{ + "name": "@stock-bot/cache", + "version": "1.0.0", + "description": "Caching library for Redis and in-memory providers", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsc --watch" + }, + "dependencies": { + "ioredis": "^5.3.2" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "typescript": "^5.3.0" + }, + "peerDependencies": { + "bun-types": "*" + } +} diff --git a/libs/cache/src/index.ts b/libs/cache/src/index.ts new file mode 100644 index 0000000..205e474 --- /dev/null +++ b/libs/cache/src/index.ts @@ -0,0 +1,26 @@ +import { RedisCache } from './providers/redis-cache'; +import { MemoryCache } from './providers/memory-cache'; +import type { CacheProvider, CacheOptions } from './types'; + +/** + * Factory for creating cache providers. + * + * @param type 'redis' | 'memory' + * @param options configuration for the cache + */ +export function createCache( + type: 'redis' | 'memory', + options: CacheOptions = {} +): CacheProvider { + if (type === 'redis') { + return new RedisCache(options); + } + return new MemoryCache(options); +} + +export { + CacheProvider, + CacheOptions, + RedisCache, + MemoryCache +}; diff --git a/libs/cache/src/providers/memory-cache.ts b/libs/cache/src/providers/memory-cache.ts new file mode 100644 index 0000000..867f798 --- /dev/null +++ b/libs/cache/src/providers/memory-cache.ts @@ -0,0 +1,48 @@ +import { CacheProvider } from '../types'; + +/** + * Simple in-memory cache provider. + */ +export class MemoryCache implements CacheProvider { + private store = new Map(); + private defaultTTL: number; + private keyPrefix: string; + + constructor(options: { ttl?: number; keyPrefix?: string } = {}) { + this.defaultTTL = options.ttl ?? 3600; + this.keyPrefix = options.keyPrefix ?? 'cache:'; + } + + private getKey(key: string): string { + return `${this.keyPrefix}${key}`; + } + + async get(key: string): Promise { + const fullKey = this.getKey(key); + const entry = this.store.get(fullKey); + if (!entry) return null; + if (entry.expiry < Date.now()) { + this.store.delete(fullKey); + return null; + } + return entry.value; + } + + async set(key: string, value: T, ttl?: number): Promise { + const fullKey = this.getKey(key); + const expiry = Date.now() + 1000 * (ttl ?? this.defaultTTL); + this.store.set(fullKey, { value, expiry }); + } + + async del(key: string): Promise { + this.store.delete(this.getKey(key)); + } + + async exists(key: string): Promise { + return (await this.get(key)) !== null; + } + + async clear(): Promise { + this.store.clear(); + } +} diff --git a/libs/cache/src/providers/redis-cache.ts b/libs/cache/src/providers/redis-cache.ts new file mode 100644 index 0000000..f5862c4 --- /dev/null +++ b/libs/cache/src/providers/redis-cache.ts @@ -0,0 +1,59 @@ +import Redis, { RedisOptions } from 'ioredis'; +import { CacheProvider, CacheOptions } from '../types'; + +/** + * Redis-based cache provider implementing CacheProvider interface. + */ +export class RedisCache implements CacheProvider { + private redis: Redis; + private defaultTTL: number; + private keyPrefix: string; + + constructor(options: CacheOptions = {}) { + if (options.redisUrl) { + this.redis = new Redis(options.redisUrl); + } else { + this.redis = new Redis(options.redisOptions as RedisOptions); + } + + this.defaultTTL = options.ttl ?? 3600; // default 1 hour + this.keyPrefix = options.keyPrefix ?? 'cache:'; + } + + private getKey(key: string): string { + return `${this.keyPrefix}${key}`; + } + + async get(key: string): Promise { + const fullKey = this.getKey(key); + const val = await this.redis.get(fullKey); + if (val === null) return null; + try { + return JSON.parse(val) as T; + } catch { + return (val as unknown) as T; + } + } + + async set(key: string, value: T, ttl?: number): Promise { + const fullKey = this.getKey(key); + const str = typeof value === 'string' ? (value as unknown as string) : JSON.stringify(value); + const expiry = ttl ?? this.defaultTTL; + await this.redis.set(fullKey, str, 'EX', expiry); + } + + async del(key: string): Promise { + await this.redis.del(this.getKey(key)); + } + + async exists(key: string): Promise { + const exists = await this.redis.exists(this.getKey(key)); + return exists === 1; + } + + async clear(): Promise { + const pattern = `${this.keyPrefix}*`; + const keys = await this.redis.keys(pattern); + if (keys.length) await this.redis.del(...keys); + } +} diff --git a/libs/cache/src/types.ts b/libs/cache/src/types.ts new file mode 100644 index 0000000..5613474 --- /dev/null +++ b/libs/cache/src/types.ts @@ -0,0 +1,34 @@ +import type { RedisOptions as IORedisOptions } from 'ioredis'; + +/** + * Interface for a generic cache provider. + */ +export interface CacheProvider { + get(key: string): Promise; + set(key: string, value: T, ttl?: number): Promise; + del(key: string): Promise; + exists(key: string): Promise; + clear(): Promise; +} + +/** + * Options for configuring the cache provider. + */ +export interface CacheOptions { + /** + * Full Redis connection string (e.g., redis://localhost:6379) + */ + redisUrl?: string; + /** + * Raw ioredis connection options if not using a URL. + */ + redisOptions?: IORedisOptions; + /** + * Default time-to-live for cache entries (in seconds). + */ + ttl?: number; + /** + * Prefix to use for all cache keys. + */ + keyPrefix?: string; +} diff --git a/libs/cache/tsconfig.json b/libs/cache/tsconfig.json new file mode 100644 index 0000000..2e0bad5 --- /dev/null +++ b/libs/cache/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] +} diff --git a/package.json b/package.json index 99f168e..b179615 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "turbo run dev", "build": "turbo run build", - "build:libs": "pwsh ./scripts/build-libs.ps1", + "build:libs": "powershell ./scripts/build-libs.ps1", "test": "turbo run test", "test:watch": "bun test --watch", "test:coverage": "bun test --coverage", @@ -19,14 +19,14 @@ "clean": "turbo run clean", "start": "turbo run start", "backtest": "turbo run backtest", - "docker:start": "pwsh ./scripts/docker.ps1 start", - "docker:stop": "pwsh ./scripts/docker.ps1 stop", - "docker:restart": "pwsh ./scripts/docker.ps1 restart", - "docker:status": "pwsh ./scripts/docker.ps1 status", - "docker:logs": "pwsh ./scripts/docker.ps1 logs", - "docker:reset": "pwsh ./scripts/docker.ps1 reset", - "docker:admin": "pwsh ./scripts/docker.ps1 admin", - "docker:monitoring": "pwsh ./scripts/docker.ps1 monitoring", + "docker:start": "powershell ./scripts/docker.ps1 start", + "docker:stop": "powershell ./scripts/docker.ps1 stop", + "docker:restart": "powershell ./scripts/docker.ps1 restart", + "docker:status": "powershell ./scripts/docker.ps1 status", + "docker:logs": "powershell ./scripts/docker.ps1 logs", + "docker:reset": "powershell ./scripts/docker.ps1 reset", + "docker:admin": "powershell ./scripts/docker.ps1 admin", + "docker:monitoring": "powershell ./scripts/docker.ps1 monitoring", "infra:up": "docker-compose up -d dragonfly postgres questdb mongodb", "infra:down": "docker-compose down", "infra:reset": "docker-compose down -v && docker-compose up -d dragonfly postgres questdb mongodb", diff --git a/scripts/build-libs.ps1 b/scripts/build-libs.ps1 index a0ff529..6316565 100644 --- a/scripts/build-libs.ps1 +++ b/scripts/build-libs.ps1 @@ -8,6 +8,7 @@ $libs = @( "logger", # Logging utilities - depends on types "config", # Configuration - depends on types "utils", # Utilities - depends on types and config + "cache", # Cache - depends on types and logger "http-client", # HTTP client - depends on types, config, logger "postgres-client", # PostgreSQL client - depends on types, config, logger "mongodb-client", # MongoDB client - depends on types, config, logger diff --git a/tsconfig.json b/tsconfig.json index ccf7fa6..606b8fd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -47,6 +47,8 @@ { "path": "./libs/postgres-client" }, { "path": "./libs/questdb-client" }, { "path": "./libs/types" }, + { "path": "./libs/cache" }, + { "path": "./libs/logger" }, { "path": "./libs/utils" }, { "path": "./libs/event-bus" }, { "path": "./libs/data-frame" },