@voilajsx/appkit
Version:
Minimal and framework agnostic Node.js toolkit designed for AI agentic backend development
193 lines • 8.88 kB
JavaScript
/**
* Smart defaults and environment validation for caching
* @module @voilajsx/appkit/cache
* @file src/cache/defaults.ts
*
* @llm-rule WHEN: App startup - need to configure cache behavior and connection strategy
* @llm-rule AVOID: Calling multiple times - expensive environment parsing, use lazy loading in get()
* @llm-rule NOTE: Called once at startup, cached globally for performance
*/
/**
* Gets smart defaults using environment variables with auto-strategy detection
* @llm-rule WHEN: App startup to get production-ready cache configuration
* @llm-rule AVOID: Calling repeatedly - expensive validation, cache the result
* @llm-rule NOTE: Auto-detects Redis vs Memory based on REDIS_URL environment variable
*/
export function getSmartDefaults() {
validateEnvironment();
const nodeEnv = process.env.NODE_ENV || 'development';
const isDevelopment = nodeEnv === 'development';
const isProduction = nodeEnv === 'production';
const isTest = nodeEnv === 'test';
// Auto-detect strategy from environment
const strategy = detectCacheStrategy();
return {
// Strategy selection with smart detection
strategy,
// Key management with service identification
keyPrefix: process.env.VOILA_CACHE_PREFIX || process.env.VOILA_SERVICE_NAME || 'app',
namespace: process.env.VOILA_CACHE_NAMESPACE || 'default',
// TTL configuration with environment awareness
defaultTTL: parseInt(process.env.VOILA_CACHE_TTL || (isProduction ? '3600' : '300')), // 1hr prod, 5min dev
// Redis configuration (only used when strategy is 'redis')
redis: {
url: process.env.REDIS_URL || 'redis://localhost:6379',
password: process.env.REDIS_PASSWORD,
maxRetries: parseInt(process.env.VOILA_CACHE_REDIS_RETRIES || '3'),
retryDelay: parseInt(process.env.VOILA_CACHE_REDIS_RETRY_DELAY || '1000'),
connectTimeout: parseInt(process.env.VOILA_CACHE_REDIS_CONNECT_TIMEOUT || '10000'),
commandTimeout: parseInt(process.env.VOILA_CACHE_REDIS_COMMAND_TIMEOUT || '5000'),
},
// Memory configuration (only used when strategy is 'memory')
memory: {
maxItems: parseInt(process.env.VOILA_CACHE_MEMORY_MAX_ITEMS || '10000'),
maxSizeBytes: parseInt(process.env.VOILA_CACHE_MEMORY_MAX_SIZE || '100000000'), // 100MB
checkInterval: parseInt(process.env.VOILA_CACHE_MEMORY_CHECK_INTERVAL || '60000'), // 1 minute
},
// Environment information
environment: {
isDevelopment,
isProduction,
isTest,
nodeEnv,
},
};
}
/**
* Auto-detect cache strategy from environment variables
* @llm-rule WHEN: Determining which cache strategy to use automatically
* @llm-rule AVOID: Manual strategy selection - environment detection handles most cases
* @llm-rule NOTE: REDIS_URL = Redis, no REDIS_URL = Memory (perfect for dev/test)
*/
function detectCacheStrategy() {
// Explicit override wins (for testing/debugging)
const explicit = process.env.VOILA_CACHE_STRATEGY?.toLowerCase();
if (explicit === 'redis' || explicit === 'memory') {
return explicit;
}
// Auto-detection logic
if (process.env.REDIS_URL) {
return 'redis'; // Redis URL available
}
if (process.env.NODE_ENV === 'production') {
console.warn('[VoilaJSX AppKit] No REDIS_URL found in production. ' +
'Using memory cache which will not persist across server restarts. ' +
'Set REDIS_URL for production caching.');
}
return 'memory'; // Default to memory for development/testing
}
/**
* Validates environment variables for cache configuration
* @llm-rule WHEN: App startup to ensure proper cache environment configuration
* @llm-rule AVOID: Skipping validation - improper config causes silent cache failures
* @llm-rule NOTE: Validates Redis URLs, numeric values, and production requirements
*/
function validateEnvironment() {
// Validate Redis URL if provided
const redisUrl = process.env.REDIS_URL;
if (redisUrl && !isValidRedisUrl(redisUrl)) {
throw new Error(`Invalid REDIS_URL: "${redisUrl}". Must start with redis:// or rediss://`);
}
// Validate cache strategy if explicitly set
const strategy = process.env.VOILA_CACHE_STRATEGY;
if (strategy && !['redis', 'memory'].includes(strategy.toLowerCase())) {
throw new Error(`Invalid VOILA_CACHE_STRATEGY: "${strategy}". Must be "redis" or "memory"`);
}
// Validate numeric values
validateNumericEnv('VOILA_CACHE_TTL', 1, 86400 * 7); // 1 second to 1 week
validateNumericEnv('VOILA_CACHE_REDIS_RETRIES', 0, 10);
validateNumericEnv('VOILA_CACHE_REDIS_RETRY_DELAY', 100, 10000);
validateNumericEnv('VOILA_CACHE_REDIS_CONNECT_TIMEOUT', 1000, 60000);
validateNumericEnv('VOILA_CACHE_REDIS_COMMAND_TIMEOUT', 1000, 30000);
validateNumericEnv('VOILA_CACHE_MEMORY_MAX_ITEMS', 100, 1000000);
validateNumericEnv('VOILA_CACHE_MEMORY_MAX_SIZE', 1000000, 1000000000); // 1MB to 1GB
validateNumericEnv('VOILA_CACHE_MEMORY_CHECK_INTERVAL', 10000, 300000); // 10s to 5min
// Validate key prefix
const keyPrefix = process.env.VOILA_CACHE_PREFIX;
if (keyPrefix && !/^[a-zA-Z0-9_-]+$/.test(keyPrefix)) {
throw new Error(`Invalid VOILA_CACHE_PREFIX: "${keyPrefix}". Must contain only letters, numbers, underscores, and hyphens`);
}
// Validate namespace
const namespace = process.env.VOILA_CACHE_NAMESPACE;
if (namespace && !/^[a-zA-Z0-9_-]+$/.test(namespace)) {
throw new Error(`Invalid VOILA_CACHE_NAMESPACE: "${namespace}". Must contain only letters, numbers, underscores, and hyphens`);
}
// Production-specific validations
const nodeEnv = process.env.NODE_ENV;
if (nodeEnv === 'production') {
if (!redisUrl) {
console.warn('[VoilaJSX AppKit] Production environment detected without REDIS_URL. ' +
'Memory cache will not persist across server restarts. ' +
'Consider setting REDIS_URL for production deployments.');
}
}
// Validate NODE_ENV
if (nodeEnv && !['development', 'production', 'test', 'staging'].includes(nodeEnv)) {
console.warn(`[VoilaJSX AppKit] Unusual NODE_ENV: "${nodeEnv}". ` +
`Expected: development, production, test, or staging`);
}
}
/**
* Validates Redis URL format
* @llm-rule WHEN: Checking Redis connection string validity
* @llm-rule AVOID: Using invalid Redis URLs - causes connection failures
*/
function isValidRedisUrl(url) {
try {
const parsed = new URL(url);
return ['redis:', 'rediss:'].includes(parsed.protocol);
}
catch {
return false;
}
}
/**
* Validates numeric environment variable within acceptable range
* @llm-rule WHEN: Validating cache configuration numeric values
* @llm-rule AVOID: Using values outside safe ranges - causes performance or memory issues
*/
function validateNumericEnv(name, min, max) {
const value = process.env[name];
if (!value)
return;
const num = parseInt(value);
if (isNaN(num) || num < min || num > max) {
throw new Error(`Invalid ${name}: "${value}". Must be a number between ${min} and ${max}`);
}
}
/**
* Gets cache configuration summary for debugging and health checks
* @llm-rule WHEN: Debugging cache configuration or building health check endpoints
* @llm-rule AVOID: Exposing sensitive connection details - this only shows safe info
*/
export function getConfigSummary() {
const config = getSmartDefaults();
return {
strategy: config.strategy,
keyPrefix: config.keyPrefix,
namespace: config.namespace,
defaultTTL: config.defaultTTL,
redisConnected: config.strategy === 'redis' && !!config.redis?.url,
environment: config.environment.nodeEnv,
};
}
/**
* Validates that required cache configuration is present for production
* @llm-rule WHEN: App startup validation for production deployments
* @llm-rule AVOID: Skipping validation - missing cache config causes runtime issues
*/
export function validateProductionRequirements() {
const config = getSmartDefaults();
if (config.environment.isProduction) {
if (config.strategy === 'memory') {
console.warn('[VoilaJSX AppKit] Using memory cache in production. ' +
'Data will not persist across server restarts. ' +
'Set REDIS_URL for persistent caching.');
}
if (config.strategy === 'redis' && !config.redis?.url) {
throw new Error('Redis strategy selected but REDIS_URL not configured. ' +
'Set REDIS_URL environment variable for Redis caching.');
}
}
}
//# sourceMappingURL=defaults.js.map