UNPKG

hook-engine

Version:

Production-grade webhook engine with comprehensive adapter support, security, reliability, structured logging, and CLI tools.

378 lines (377 loc) 13 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EventProcessor = void 0; const events_1 = require("events"); const webhook_errors_1 = require("../errors/webhook-errors"); class EventProcessor extends events_1.EventEmitter { constructor(config = {}) { super(); this.pipelines = new Map(); this.tenantConfigs = new Map(); this.processingQueue = []; this.isProcessing = false; this.metrics = { totalProcessed: 0, totalFailed: 0, averageProcessingTime: 0, lastProcessedAt: 0 }; this.config = { maxConcurrency: 10, defaultTimeout: 30000, enableMetrics: true, enableDeadLetterQueue: true, retryFailedEvents: true, maxRetries: 3, ...config }; } /** * Add processing pipeline */ addPipeline(pipeline) { this.pipelines.set(pipeline.id, pipeline); this.emit('pipelineAdded', pipeline); } /** * Remove processing pipeline */ removePipeline(pipelineId) { const removed = this.pipelines.delete(pipelineId); if (removed) { this.emit('pipelineRemoved', pipelineId); } return removed; } /** * Add tenant configuration */ addTenantConfig(tenantConfig) { this.tenantConfigs.set(tenantConfig.tenantId, tenantConfig); this.emit('tenantConfigAdded', tenantConfig); } /** * Process single event */ async processEvent(event, adapter) { const startTime = Date.now(); try { // Validate tenant if specified if (event.tenant && !this.validateTenantAccess(event)) { throw new webhook_errors_1.WebhookValidationError('Tenant access denied'); } // Get applicable pipelines const pipelines = this.getApplicablePipelines(event); if (pipelines.length === 0) { return { success: true, eventId: event.id, processingTime: Date.now() - startTime }; } // Process through each pipeline let transformedEvent = event; const routeResults = []; for (const pipeline of pipelines) { // Apply filters const filteredEvents = this.applyFilters([transformedEvent], pipeline.filters); if (filteredEvents.length === 0) continue; // Apply transformations const transformedEvents = await this.applyTransformations(filteredEvents, pipeline.transformations); if (transformedEvents.length > 0) { transformedEvent = transformedEvents[0]; } // Apply routing const matchingRoutes = this.getMatchingRoutes(transformedEvent, pipeline.routes); for (const route of matchingRoutes) { await this.executeRoute(transformedEvent, route); routeResults.push(route.id); } } const result = { success: true, eventId: event.id, transformedEvent, processingTime: Date.now() - startTime }; this.updateMetrics(result); this.emit('eventProcessed', result); return result; } catch (error) { const result = { success: false, eventId: event.id, error: error, processingTime: Date.now() - startTime }; this.updateMetrics(result); this.emit('eventFailed', result); if (this.config.enableDeadLetterQueue) { this.emit('deadLetter', { event, error }); } return result; } } /** * Process batch of events */ async processBatch(batch, adapter) { const startTime = Date.now(); const results = []; const errors = []; // Process events with concurrency control const chunks = this.chunkArray(batch.events, this.config.maxConcurrency); for (const chunk of chunks) { const chunkPromises = chunk.map(event => this.processEvent(event, adapter)); const chunkResults = await Promise.allSettled(chunkPromises); chunkResults.forEach((result, index) => { if (result.status === 'fulfilled') { results.push(result.value); } else { const error = result.reason; errors.push(error); results.push({ success: false, eventId: chunk[index].id, error, processingTime: 0 }); } }); } const successCount = results.filter(r => r.success).length; const failureCount = results.filter(r => !r.success).length; const batchResult = { batchId: batch.batchId, totalEvents: batch.events.length, successCount, failureCount, results, processingTime: Date.now() - startTime, errors }; this.emit('batchProcessed', batchResult); return batchResult; } /** * Get applicable pipelines for event */ getApplicablePipelines(event) { let pipelines = []; // Get tenant-specific pipelines if (event.tenant) { const tenantConfig = this.tenantConfigs.get(event.tenant); if (tenantConfig) { pipelines = tenantConfig.pipelines.filter(p => p.enabled); } } // Add global pipelines const globalPipelines = Array.from(this.pipelines.values()) .filter(p => p.enabled); pipelines = [...pipelines, ...globalPipelines]; // Sort by priority return pipelines.sort((a, b) => b.priority - a.priority); } /** * Apply filters to events */ applyFilters(events, filters) { let filteredEvents = events; for (const filter of filters) { filteredEvents = this.filterEvents(filteredEvents, filter); } return filteredEvents; } /** * Filter events based on criteria */ filterEvents(events, filter) { return events.filter(event => { // Filter by types if (filter.types && !filter.types.includes(event.type)) { return false; } // Filter by sources if (filter.sources && !filter.sources.includes(event.source)) { return false; } // Filter by tenants if (filter.tenants && event.tenant && !filter.tenants.includes(event.tenant)) { return false; } // Filter by tags if (filter.tags && event.tags) { const hasMatchingTag = filter.tags.some(tag => event.tags.includes(tag)); if (!hasMatchingTag) { return false; } } // Filter by priority if (filter.priority && event.priority && !filter.priority.includes(event.priority)) { return false; } // Apply custom filter if (filter.customFilter && !filter.customFilter(event)) { return false; } return true; }); } /** * Apply transformations to events */ async applyTransformations(events, transformations) { let transformedEvents = events; for (const transformation of transformations) { transformedEvents = await this.applyTransformation(transformedEvents, transformation); } return transformedEvents; } /** * Apply single transformation */ async applyTransformation(events, transformation) { switch (transformation.type) { case 'javascript': return this.applyJavaScriptTransformation(events, transformation.script); case 'jsonata': // Would require jsonata library return events; case 'custom': // Custom transformation logic return events; default: return events; } } /** * Apply JavaScript transformation */ applyJavaScriptTransformation(events, script) { try { const transformFunction = new Function('events', ` return (${script})(events); `); return transformFunction(events); } catch (error) { throw new webhook_errors_1.WebhookProcessingError(`JavaScript transformation failed: ${error.message}`); } } /** * Get matching routes for event */ getMatchingRoutes(event, routes) { return routes .filter(route => route.enabled) .filter(route => { const matchingEvents = this.filterEvents([event], route.filter); return matchingEvents.length > 0; }) .sort((a, b) => b.priority - a.priority); } /** * Execute route */ async executeRoute(event, route) { try { // Apply route transformation if specified let routeEvent = event; if (route.transform) { const transformed = await this.applyTransformation([event], route.transform); if (transformed.length > 0) { routeEvent = transformed[0]; } } // Execute destination await this.executeDestination(routeEvent, route.destination); this.emit('routeExecuted', { event: routeEvent, route }); } catch (error) { this.emit('routeFailed', { event, route, error }); throw error; } } /** * Execute destination */ async executeDestination(event, destination) { if (typeof destination === 'string') { // Simple webhook URL this.emit('webhookCall', { event, url: destination }); } else { // Complex destination switch (destination.type) { case 'webhook': this.emit('webhookCall', { event, config: destination.config }); break; case 'queue': this.emit('queueMessage', { event, config: destination.config }); break; case 'database': this.emit('databaseWrite', { event, config: destination.config }); break; case 'function': this.emit('functionCall', { event, config: destination.config }); break; case 'custom': this.emit('customDestination', { event, config: destination.config }); break; } } } /** * Validate tenant access */ validateTenantAccess(event) { if (!event.tenant) return true; const tenantConfig = this.tenantConfigs.get(event.tenant); if (!tenantConfig) return false; // Check allowed/blocked sources if (tenantConfig.allowedSources && !tenantConfig.allowedSources.includes(event.source)) { return false; } if (tenantConfig.blockedSources && tenantConfig.blockedSources.includes(event.source)) { return false; } return true; } /** * Update processing metrics */ updateMetrics(result) { if (!this.config.enableMetrics) return; this.metrics.totalProcessed++; if (!result.success) { this.metrics.totalFailed++; } // Update average processing time const totalTime = this.metrics.averageProcessingTime * (this.metrics.totalProcessed - 1) + result.processingTime; this.metrics.averageProcessingTime = totalTime / this.metrics.totalProcessed; this.metrics.lastProcessedAt = Date.now(); } /** * Get processing metrics */ getMetrics() { return { ...this.metrics }; } /** * Utility: Chunk array into smaller arrays */ chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } } exports.EventProcessor = EventProcessor;