UNPKG

@voilajsx/appkit

Version:

Minimal and framework agnostic Node.js toolkit designed for AI agentic backend development

302 lines 11.4 kB
/** * Ultra-simple event-driven architecture that just works with automatic Redis/Memory strategy * @module @voilajsx/appkit/event * @file src/event/index.ts * * @llm-rule WHEN: Building apps that need event-driven architecture with zero configuration * @llm-rule AVOID: Complex event setups - this auto-detects Redis/Memory from environment * @llm-rule NOTE: Uses eventClass.get() pattern like auth - get() → event.emit() → distributed * @llm-rule NOTE: Common pattern - eventClass.get(namespace) → event.on() → event.emit() → handled */ import { EventClass } from './event.js'; import { getSmartDefaults, validateProductionRequirements, validateStartupConfiguration, performHealthCheck } from './defaults.js'; // Global event instances for performance (like auth module) let globalConfig = null; const namedEvents = new Map(); /** * Get event instance for specific namespace - the only function you need to learn * Strategy auto-detected from environment (REDIS_URL → Redis, no URL → Memory) * @llm-rule WHEN: Need event-driven architecture in any part of your app - this is your main entry point * @llm-rule AVOID: Creating EventClass directly - always use this function * @llm-rule NOTE: Typical flow - get(namespace) → event.on() → event.emit() → distributed handling */ function get(namespace = 'default') { // Validate namespace if (!namespace || typeof namespace !== 'string') { throw new Error('Event namespace must be a non-empty string'); } if (!/^[a-zA-Z0-9_-]+$/.test(namespace)) { throw new Error('Event namespace must contain only letters, numbers, underscores, and hyphens'); } // Lazy initialization - parse environment once (like auth) if (!globalConfig) { globalConfig = getSmartDefaults(); } // Return cached instance if exists if (namedEvents.has(namespace)) { return namedEvents.get(namespace); } // Create new event instance for namespace const eventInstance = new EventClass(globalConfig, namespace); // Auto-connect on first use eventInstance.connect().catch((error) => { console.error(`[AppKit] Event auto-connect failed for namespace "${namespace}":`, error.message); }); namedEvents.set(namespace, eventInstance); return eventInstance; } /** * Clear all event instances and disconnect - essential for testing * @llm-rule WHEN: Testing event logic with different configurations or app shutdown * @llm-rule AVOID: Using in production except for graceful shutdown */ async function clear() { const disconnectPromises = []; for (const [namespace, event] of namedEvents) { disconnectPromises.push(event.disconnect().catch((error) => { console.error(`[AppKit] Event disconnect failed for namespace "${namespace}":`, error.message); })); } await Promise.all(disconnectPromises); namedEvents.clear(); globalConfig = null; } /** * Reset event configuration (useful for testing) * @llm-rule WHEN: Testing event logic with different environment configurations * @llm-rule AVOID: Using in production - only for tests and development */ async function reset(newConfig) { // Clear existing instances await clear(); // Reset configuration if (newConfig) { const defaults = getSmartDefaults(); globalConfig = { ...defaults, ...newConfig }; } else { globalConfig = null; // Will reload from environment on next get() } } /** * Get active event strategy for debugging * @llm-rule WHEN: Debugging or health checks to see which strategy is active (Redis vs Memory) * @llm-rule AVOID: Using for application logic - events should be transparent */ function getStrategy() { if (!globalConfig) { globalConfig = getSmartDefaults(); } return globalConfig.strategy; } /** * Get all active event namespaces * @llm-rule WHEN: Debugging or monitoring which event namespaces are active * @llm-rule AVOID: Using for business logic - this is for observability only */ function getActiveNamespaces() { return Array.from(namedEvents.keys()); } /** * Get event configuration summary for debugging * @llm-rule WHEN: Health checks or debugging event configuration * @llm-rule AVOID: Exposing sensitive connection details - this only shows safe info */ function getConfig() { if (!globalConfig) { globalConfig = getSmartDefaults(); } return { strategy: globalConfig.strategy, historyEnabled: globalConfig.history.enabled, activeNamespaces: getActiveNamespaces(), environment: globalConfig.environment.nodeEnv, }; } /** * Check if Redis is available and configured * @llm-rule WHEN: Conditional logic based on event capabilities * @llm-rule AVOID: Complex event detection - just use events normally, strategy handles it */ function hasRedis() { return !!process.env.REDIS_URL; } /** * Emit event across all namespaces (dangerous) * @llm-rule WHEN: Broadcasting system-wide events like shutdown or maintenance * @llm-rule AVOID: Using for regular events - use namespace-specific events instead * @llm-rule NOTE: Only use for system-level events that need global broadcast */ async function broadcast(event, data = {}) { const activeInstances = Array.from(namedEvents.values()); if (activeInstances.length === 0) { console.warn(`[AppKit] No active event namespaces for broadcast: ${event}`); return []; } const results = await Promise.allSettled(activeInstances.map(instance => instance.emit(event, data))); return results.map(result => result.status === 'fulfilled' ? result.value : false); } /** * Get event statistics across all namespaces * @llm-rule WHEN: Monitoring event system health and usage * @llm-rule AVOID: Using for business logic - this is for monitoring only */ function getStats() { const strategy = getStrategy(); const namespaces = Array.from(namedEvents.entries()).map(([namespace, instance]) => { const config = instance.getConfig(); const listeners = instance.getListeners(); return { namespace, listeners: listeners.totalListeners || 0, connected: config.connected, }; }); return { strategy, totalNamespaces: namespaces.length, totalListeners: namespaces.reduce((sum, ns) => sum + ns.listeners, 0), connected: namespaces.filter(ns => ns.connected).length, namespaces, }; } /** * Validate event configuration at startup with detailed feedback * @llm-rule WHEN: App startup to ensure events are properly configured * @llm-rule AVOID: Skipping validation - missing event config causes runtime issues * @llm-rule NOTE: Returns validation results instead of throwing - allows graceful handling */ function validateConfig() { try { const validation = validateStartupConfiguration(); if (validation.errors.length > 0) { console.error('[VoilaJSX AppKit] Event configuration errors:', validation.errors); } if (validation.warnings.length > 0) { console.warn('[VoilaJSX AppKit] Event configuration warnings:', validation.warnings); } if (validation.ready) { console.log(`✅ [VoilaJSX AppKit] Events configured with ${validation.strategy} strategy`); } return { valid: validation.errors.length === 0, strategy: validation.strategy, warnings: validation.warnings, errors: validation.errors, ready: validation.ready, }; } catch (error) { const errorMessage = error.message; console.error('[VoilaJSX AppKit] Event configuration validation failed:', errorMessage); return { valid: false, strategy: 'unknown', warnings: [], errors: [errorMessage], ready: false, }; } } /** * Validate production requirements and throw if critical issues found * @llm-rule WHEN: Production deployment validation - ensures events work in production * @llm-rule AVOID: Skipping in production - event failures are often silent * @llm-rule NOTE: Throws on critical issues, warns on non-critical ones */ function validateProduction() { try { validateProductionRequirements(); if (process.env.NODE_ENV === 'production' && !hasRedis()) { console.warn('[VoilaJSX AppKit] No Redis configured in production. ' + 'Set REDIS_URL for distributed events across servers.'); } console.log('✅ [VoilaJSX AppKit] Production event requirements validated'); } catch (error) { console.error('[VoilaJSX AppKit] Production event validation failed:', error.message); throw error; } } /** * Get comprehensive health check status for monitoring * @llm-rule WHEN: Health check endpoints or monitoring systems * @llm-rule AVOID: Using in critical application path - this is for monitoring only * @llm-rule NOTE: Returns detailed status without exposing sensitive configuration */ function getHealthStatus() { return performHealthCheck(); } /** * Graceful shutdown for all event instances * @llm-rule WHEN: App shutdown or process termination * @llm-rule AVOID: Abrupt process exit - graceful shutdown prevents data loss */ async function shutdown() { console.log('🔄 [AppKit] Event graceful shutdown...'); try { // Broadcast shutdown event before closing await broadcast('system.shutdown', { timestamp: new Date().toISOString(), reason: 'graceful_shutdown' }); // Small delay to allow event processing await new Promise(resolve => setTimeout(resolve, 100)); // Clear all instances await clear(); console.log('✅ [AppKit] Event shutdown complete'); } catch (error) { console.error('❌ [AppKit] Event shutdown error:', error.message); } } /** * Single eventing export with minimal API (like auth module) */ export const eventClass = { // Core method (like auth.get()) get, // Utility methods clear, reset, getStrategy, getActiveNamespaces, getConfig, hasRedis, getStats, // Advanced methods broadcast, // Validation and lifecycle validateConfig, validateProduction, getHealthStatus, shutdown, }; export { EventClass } from './event.js'; // Default export export default eventClass; // Auto-setup graceful shutdown handlers if (typeof process !== 'undefined') { // Handle graceful shutdown const shutdownHandler = () => { shutdown().finally(() => { process.exit(0); }); }; process.on('SIGTERM', shutdownHandler); process.on('SIGINT', shutdownHandler); // Handle uncaught errors process.on('uncaughtException', (error) => { console.error('[AppKit] Uncaught exception during event operation:', error); shutdown().finally(() => { process.exit(1); }); }); process.on('unhandledRejection', (reason) => { console.error('[AppKit] Unhandled rejection during event operation:', reason); shutdown().finally(() => { process.exit(1); }); }); } //# sourceMappingURL=index.js.map