@voilajsx/appkit
Version:
Minimal and framework agnostic Node.js toolkit designed for AI agentic backend development
378 lines • 15.7 kB
JavaScript
/**
* Smart defaults and environment validation for event system with auto-strategy detection
* @module @voilajsx/appkit/event
* @file src/event/defaults.ts
*
* @llm-rule WHEN: App startup - need to configure event system 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
* @llm-rule NOTE: Auto-detects Redis vs Memory based on REDIS_URL environment variable
*/
/**
* Gets smart defaults using environment variables with auto-strategy detection
* @llm-rule WHEN: App startup to get production-ready event configuration
* @llm-rule AVOID: Calling repeatedly - expensive validation, cache the result
* @llm-rule NOTE: Auto-detects strategy: REDIS_URL → Redis, no REDIS_URL → Memory
*/
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 = detectEventStrategy();
return {
// Strategy selection with smart detection
strategy,
// Namespace with service identification
namespace: process.env.VOILA_EVENT_NAMESPACE || process.env.VOILA_SERVICE_NAME || 'default',
// 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_EVENT_REDIS_RETRIES || '3'),
retryDelay: parseInt(process.env.VOILA_EVENT_REDIS_RETRY_DELAY || '1000'),
connectTimeout: parseInt(process.env.VOILA_EVENT_REDIS_CONNECT_TIMEOUT || '10000'),
commandTimeout: parseInt(process.env.VOILA_EVENT_REDIS_COMMAND_TIMEOUT || '5000'),
keyPrefix: process.env.VOILA_EVENT_REDIS_PREFIX || 'events',
},
// Memory configuration (only used when strategy is 'memory')
memory: {
maxListeners: parseInt(process.env.VOILA_EVENT_MEMORY_MAX_LISTENERS || '1000'),
maxHistory: parseInt(process.env.VOILA_EVENT_MEMORY_HISTORY || '100'),
checkInterval: parseInt(process.env.VOILA_EVENT_MEMORY_CHECK_INTERVAL || '30000'), // 30 seconds
enableGC: process.env.VOILA_EVENT_MEMORY_GC !== 'false',
},
// Event history configuration
history: {
enabled: process.env.VOILA_EVENT_HISTORY_ENABLED !== 'false',
maxSize: parseInt(process.env.VOILA_EVENT_HISTORY_SIZE || (isProduction ? '50' : '100')),
},
// Environment information
environment: {
isDevelopment,
isProduction,
isTest,
nodeEnv,
},
};
}
/**
* Auto-detect event strategy from environment variables
* @llm-rule WHEN: Determining which event strategy to use automatically
* @llm-rule AVOID: Manual strategy selection - environment detection handles most cases
* @llm-rule NOTE: Priority: REDIS_URL → Redis, no REDIS_URL → Memory (perfect for dev/prod)
*/
function detectEventStrategy() {
// Explicit override wins (for testing/debugging)
const explicit = process.env.VOILA_EVENT_STRATEGY?.toLowerCase();
if (explicit === 'redis' || explicit === 'memory') {
return explicit;
}
// Auto-detection logic
if (process.env.REDIS_URL) {
return 'redis'; // Redis URL available - use distributed events
}
// Default to memory for development/testing
if (process.env.NODE_ENV === 'production') {
console.warn('[VoilaJSX AppKit] No REDIS_URL found in production. ' +
'Using memory event strategy which will not work across multiple servers. ' +
'Set REDIS_URL for distributed events.');
}
return 'memory'; // Default to memory for single-server setups
}
/**
* Validates environment variables for event configuration
* @llm-rule WHEN: App startup to ensure proper event environment configuration
* @llm-rule AVOID: Skipping validation - improper config causes silent event failures
* @llm-rule NOTE: Validates Redis URLs, numeric values, and production requirements
*/
function validateEnvironment() {
// Validate event strategy if explicitly set
const strategy = process.env.VOILA_EVENT_STRATEGY;
if (strategy && !['redis', 'memory'].includes(strategy.toLowerCase())) {
throw new Error(`Invalid VOILA_EVENT_STRATEGY: "${strategy}". Must be "redis" or "memory"`);
}
// 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 namespace
const namespace = process.env.VOILA_EVENT_NAMESPACE;
if (namespace && !/^[a-zA-Z0-9_-]+$/.test(namespace)) {
throw new Error(`Invalid VOILA_EVENT_NAMESPACE: "${namespace}". Must contain only letters, numbers, underscores, and hyphens`);
}
// Validate numeric values
validateNumericEnv('VOILA_EVENT_REDIS_RETRIES', 0, 10);
validateNumericEnv('VOILA_EVENT_REDIS_RETRY_DELAY', 100, 10000);
validateNumericEnv('VOILA_EVENT_REDIS_CONNECT_TIMEOUT', 1000, 60000);
validateNumericEnv('VOILA_EVENT_REDIS_COMMAND_TIMEOUT', 1000, 30000);
validateNumericEnv('VOILA_EVENT_MEMORY_MAX_LISTENERS', 10, 10000);
validateNumericEnv('VOILA_EVENT_MEMORY_HISTORY', 10, 1000);
validateNumericEnv('VOILA_EVENT_MEMORY_CHECK_INTERVAL', 5000, 300000); // 5s to 5min
validateNumericEnv('VOILA_EVENT_HISTORY_SIZE', 1, 1000);
// Validate Redis key prefix
const keyPrefix = process.env.VOILA_EVENT_REDIS_PREFIX;
if (keyPrefix && !/^[a-zA-Z0-9_-]+$/.test(keyPrefix)) {
throw new Error(`Invalid VOILA_EVENT_REDIS_PREFIX: "${keyPrefix}". Must contain only letters, numbers, underscores, and hyphens`);
}
// Production-specific validations
const nodeEnv = process.env.NODE_ENV;
if (nodeEnv === 'production') {
validateProductionConfig();
}
// 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 event 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}`);
}
}
/**
* Validates production event configuration
* @llm-rule WHEN: Running in production environment
* @llm-rule AVOID: Memory strategy in multi-server production - events won't work across servers
*/
function validateProductionConfig() {
const strategy = detectEventStrategy();
if (strategy === 'memory') {
console.warn('[VoilaJSX AppKit] Using memory event strategy in production. ' +
'Events will only work within single server instance. ' +
'Set REDIS_URL for distributed events across multiple servers.');
}
// Validate namespace is set in production
const namespace = process.env.VOILA_EVENT_NAMESPACE;
if (!namespace) {
console.warn('[VoilaJSX AppKit] No event namespace configured in production. ' +
'Set VOILA_EVENT_NAMESPACE for proper event isolation.');
}
}
/**
* Gets event configuration summary for debugging and health checks
* @llm-rule WHEN: Debugging event 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,
namespace: config.namespace,
historyEnabled: config.history.enabled,
redisConnected: config.strategy === 'redis' && !!config.redis?.url,
environment: config.environment.nodeEnv,
};
}
/**
* Validates that required event configuration is present for production
* @llm-rule WHEN: App startup validation for production deployments
* @llm-rule AVOID: Skipping validation - missing event config causes runtime issues
*/
export function validateProductionRequirements() {
const config = getSmartDefaults();
if (config.environment.isProduction) {
if (config.strategy === 'memory') {
console.warn('[VoilaJSX AppKit] Using memory event strategy in production. ' +
'Events will not work across multiple server instances. ' +
'Set REDIS_URL for distributed events.');
}
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 events.');
}
}
}
/**
* Validates startup configuration and throws detailed errors
* @llm-rule WHEN: App startup to ensure event configuration is valid before starting
* @llm-rule AVOID: Skipping validation - catches config issues early
* @llm-rule NOTE: Comprehensive validation for production readiness
*/
export function validateStartupConfiguration() {
const warnings = [];
const errors = [];
try {
const config = getSmartDefaults();
const strategy = config.strategy;
// Strategy-specific validation
switch (strategy) {
case 'redis':
if (!config.redis?.url) {
errors.push('REDIS_URL is required for Redis strategy');
}
else if (!isValidRedisUrl(config.redis.url)) {
errors.push('REDIS_URL must start with redis:// or rediss://');
}
break;
case 'memory':
if (config.environment.isProduction) {
warnings.push('Memory strategy in production - events will not work across servers');
}
break;
}
// Namespace validation
if (!config.namespace || config.namespace === 'default') {
if (config.environment.isProduction) {
warnings.push('Using default namespace in production - consider setting VOILA_EVENT_NAMESPACE');
}
}
// Environment-specific warnings
if (config.environment.isProduction && strategy === 'memory') {
warnings.push('No Redis configured for distributed events in production');
}
if (config.environment.isDevelopment && strategy === 'redis') {
warnings.push('Using Redis in development - memory strategy is faster for local development');
}
return {
strategy,
warnings,
errors,
ready: errors.length === 0,
};
}
catch (error) {
errors.push(`Configuration validation failed: ${error.message}`);
return {
strategy: 'unknown',
warnings,
errors,
ready: false,
};
}
}
/**
* Performs comprehensive event system health check
* @llm-rule WHEN: Health check endpoints or monitoring systems
* @llm-rule AVOID: Running in critical path - this is for monitoring only
* @llm-rule NOTE: Returns detailed health status without exposing sensitive data
*/
export function performHealthCheck() {
const issues = [];
let status = 'healthy';
try {
const validation = validateStartupConfiguration();
// Determine overall status
if (validation.errors.length > 0) {
status = 'error';
issues.push(...validation.errors);
}
else if (validation.warnings.length > 0) {
status = 'warning';
issues.push(...validation.warnings);
}
const configured = validation.strategy !== 'memory' ||
process.env.NODE_ENV !== 'production';
return {
status,
strategy: validation.strategy,
configured,
issues,
ready: validation.ready,
timestamp: new Date().toISOString(),
};
}
catch (error) {
return {
status: 'error',
strategy: 'unknown',
configured: false,
issues: [`Health check failed: ${error.message}`],
ready: false,
timestamp: new Date().toISOString(),
};
}
}
/**
* Gets optimal event configuration for different environments
* @llm-rule WHEN: Setting up environment-specific event behavior
* @llm-rule AVOID: Manual environment handling - this provides optimal defaults
*/
export function getEnvironmentOptimizedConfig() {
const config = getSmartDefaults();
// Optimize for different environments
if (config.environment.isDevelopment) {
// Development: More history, frequent GC
config.history.maxSize = 100;
if (config.memory) {
config.memory.checkInterval = 10000; // 10 seconds
config.memory.enableGC = true;
}
}
else if (config.environment.isProduction) {
// Production: Less history, less frequent GC
config.history.maxSize = 50;
if (config.memory) {
config.memory.checkInterval = 60000; // 1 minute
config.memory.enableGC = true;
}
}
else if (config.environment.isTest) {
// Test: Minimal history, no GC
config.history.maxSize = 10;
if (config.memory) {
config.memory.checkInterval = 1000; // 1 second
config.memory.enableGC = false;
}
}
return config;
}
/**
* Checks if Redis is available and properly configured
* @llm-rule WHEN: Conditional logic based on event capabilities
* @llm-rule AVOID: Complex event detection - just use events normally, strategy handles it
*/
export function hasRedis() {
const redisUrl = process.env.REDIS_URL;
return !!(redisUrl && isValidRedisUrl(redisUrl));
}
/**
* Gets recommended configuration for microservices
* @llm-rule WHEN: Setting up events for microservices architecture
* @llm-rule AVOID: Default config for microservices - needs specific tuning
*/
export function getMicroservicesConfig() {
return {
strategy: 'redis', // Always use Redis for microservices
history: {
enabled: true,
maxSize: 25, // Less history per service
},
redis: {
url: process.env.REDIS_URL || 'redis://localhost:6379',
maxRetries: 5, // More retries for reliability
retryDelay: 2000, // Longer delays
connectTimeout: 15000, // Longer timeout
commandTimeout: 10000, // Longer command timeout
keyPrefix: 'microservices:events',
},
};
}
//# sourceMappingURL=defaults.js.map