UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

441 lines (361 loc) 13 kB
import { EventEmitter } from 'events'; import { ProcessEngine } from './process-engine.js'; import { ProcessStore } from './process-store.js'; import { ProcessDefinition, ProcessTrigger, TriggerType } from './types.js'; import { CronAdapter, NodeCronAdapter, CronTask } from './cron-adapter.js'; interface ScheduledTrigger { processId: string; triggerId: string; interval?: NodeJS.Timeout; nextRun?: Date; } interface TriggerExecution { triggerId: string; processId: string; executionId: string; executedAt: string; status: 'success' | 'failed'; } export class TriggerManager extends EventEmitter { private engine: ProcessEngine; private store: ProcessStore; private scheduledTriggers: Map<string, ScheduledTrigger> = new Map(); private activeTriggers: Map<string, ProcessTrigger> = new Map(); private triggerHistory: Map<string, TriggerExecution[]> = new Map(); private isRunning: boolean = false; private checkInterval?: NodeJS.Timeout; private cronAdapter: CronAdapter; constructor(engine: ProcessEngine, store: ProcessStore, cronAdapter?: CronAdapter) { super(); this.engine = engine; this.store = store; this.cronAdapter = cronAdapter || new NodeCronAdapter(); } async initialize(): Promise<void> { await this.start(); } async start(): Promise<void> { if (this.isRunning) return; this.isRunning = true; await this.loadTriggers(); // Check for triggers every minute this.checkInterval = setInterval(() => { this.checkTriggers(); }, 60000); // Initial check this.checkTriggers(); } async stop(): Promise<void> { this.isRunning = false; if (this.checkInterval) { clearInterval(this.checkInterval); } // Clear all scheduled triggers for (const scheduled of this.scheduledTriggers.values()) { if (scheduled.interval) { // If it's a cron task, call destroy if ((scheduled.interval as any).destroy) { (scheduled.interval as any).destroy(); } else { clearInterval(scheduled.interval); } } } this.scheduledTriggers.clear(); } private async loadTriggers(): Promise<void> { const processes = await this.store.getAllProcesses(); for (const process of processes) { for (const trigger of process.triggers) { if (trigger.enabled) { await this.registerTrigger(process, trigger); } } } } private async registerTriggerInternal( process: ProcessDefinition, trigger: ProcessTrigger ): Promise<void> { const key = `${process.id}:${trigger.id}`; switch (trigger.type) { case 'schedule': this.registerScheduleTrigger(process, trigger); break; case 'event': // Register event listeners this.registerEventTrigger(process, trigger); break; case 'webhook': // Register webhook endpoint this.registerWebhookTrigger(process, trigger); break; case 'condition': // Add to condition check list this.registerConditionTrigger(process, trigger); break; case 'manual': // No registration needed for manual triggers break; } } private registerScheduleTrigger( process: ProcessDefinition, trigger: ProcessTrigger ): void { if (!trigger.config.cron) return; const key = `${process.id}:${trigger.id}`; // Simple cron parser for demo (in production use node-cron) const schedule = this.parseCronExpression(trigger.config.cron); if (schedule.interval) { const scheduled: ScheduledTrigger = { processId: process.id, triggerId: trigger.id, interval: setInterval(() => { this.executeTrigger(process.id, trigger.id); }, schedule.interval), nextRun: schedule.nextRun }; this.scheduledTriggers.set(key, scheduled); } } private registerEventTrigger( process: ProcessDefinition, trigger: ProcessTrigger ): void { // In a real implementation, this would register with an event bus console.log(`Registered event trigger: ${trigger.config.event} for process ${process.id}`); } private registerWebhookTrigger( process: ProcessDefinition, trigger: ProcessTrigger ): void { // In a real implementation, this would register an HTTP endpoint console.log(`Registered webhook trigger: ${trigger.config.endpoint} for process ${process.id}`); } private registerConditionTrigger( process: ProcessDefinition, trigger: ProcessTrigger ): void { // Add to list of conditions to check periodically console.log(`Registered condition trigger for process ${process.id}`); } private async checkTriggers(): Promise<void> { // Check condition-based triggers const processes = await this.store.getAllProcesses(); for (const process of processes) { for (const trigger of process.triggers) { if (trigger.type === 'condition' && trigger.enabled) { await this.checkConditionTrigger(process, trigger); } } } } private async checkConditionTrigger( process: ProcessDefinition, trigger: ProcessTrigger ): Promise<void> { // Evaluate condition // In a real implementation, this would evaluate the condition expression const conditionMet = false; // Placeholder if (conditionMet) { await this.executeTrigger(process.id, trigger.id); } } private parseCronExpression(cron: string): { interval?: number; nextRun?: Date } { // Very simplified cron parser for demo // In production, use a proper cron library const parts = cron.split(' '); // Handle simple cases if (cron === '* * * * *') { // Every minute return { interval: 60000 }; } else if (cron === '0 * * * *') { // Every hour return { interval: 3600000 }; } else if (cron === '0 0 * * *') { // Daily return { interval: 86400000 }; } // For more complex expressions, would need proper parsing return {}; } // Public methods for manual trigger management async registerTrigger(process: ProcessDefinition, trigger: ProcessTrigger): Promise<void> { if (!trigger.enabled) return; const key = `${process.id}:${trigger.id}`; this.activeTriggers.set(key, trigger); if (trigger.type === 'schedule' && trigger.config.cron) { // Use node-cron for scheduling if (!this.cronAdapter.validate(trigger.config.cron)) { throw new Error(`Invalid cron expression: ${trigger.config.cron}`); } const task = this.cronAdapter.schedule(trigger.config.cron, async () => { await this.executeTrigger(process.id, trigger.id); }, { scheduled: false }); task.start(); this.scheduledTriggers.set(key, { processId: process.id, triggerId: trigger.id, interval: task as any }); } } async unregisterTrigger(processId: string, triggerId: string): Promise<void> { const key = `${processId}:${triggerId}`; this.activeTriggers.delete(key); const scheduled = this.scheduledTriggers.get(key); if (scheduled && scheduled.interval) { const task = scheduled.interval as any; if (task.stop) task.stop(); if (task.destroy) task.destroy(); this.scheduledTriggers.delete(key); } } getActiveTriggers(processId?: string): Array<ProcessTrigger & { processId: string }> { const triggers: Array<ProcessTrigger & { processId: string }> = []; for (const [key, trigger] of this.activeTriggers) { const [pid] = key.split(':'); if (!processId || pid === processId) { triggers.push({ ...trigger, processId: pid }); } } return triggers; } async executeTrigger(processId: string, triggerId: string, context?: Record<string, any>): Promise<any> { const process = await this.store.getProcess(processId); if (!process) { throw new Error('Process not found'); } const trigger = process.triggers.find(t => t.id === triggerId); if (!trigger) { throw new Error('Trigger not found'); } if (!trigger.enabled) { throw new Error('Trigger is disabled'); } const execution = await this.engine.executeProcess(process, triggerId, context); // Record in history const key = `${processId}:${triggerId}`; if (!this.triggerHistory.has(key)) { this.triggerHistory.set(key, []); } const history = this.triggerHistory.get(key)!; history.unshift({ triggerId, processId, executionId: execution.id, executedAt: new Date().toISOString(), status: execution.status === 'completed' ? 'success' : 'failed' }); // Keep only last 100 entries if (history.length > 100) { history.splice(100); } return execution; } getTriggerHistory(processId: string, triggerId: string): TriggerExecution[] { const key = `${processId}:${triggerId}`; return this.triggerHistory.get(key) || []; } async updateTrigger(processId: string, triggerId: string, updates: Partial<ProcessTrigger>): Promise<void> { const process = await this.store.getProcess(processId); if (!process) { throw new Error('Process not found'); } const trigger = process.triggers.find(t => t.id === triggerId); if (!trigger) { throw new Error('Trigger not found'); } // If disabling, unregister first if (updates.enabled === false && trigger.enabled) { await this.unregisterTrigger(processId, triggerId); } // Update the trigger Object.assign(trigger, updates); // Save the updated process await this.store.saveProcess(process); // If enabling or if already enabled and config changed, re-register if (trigger.enabled) { // Unregister first to clean up old configuration await this.unregisterTrigger(processId, triggerId); // Re-register with new configuration await this.registerTrigger(process, trigger); } } async handleEvent(eventType: string, eventData: any): Promise<void> { for (const [key, trigger] of this.activeTriggers) { if (trigger.type === 'event' && trigger.config.event === eventType) { const [processId] = key.split(':'); const process = await this.store.getProcess(processId); if (process) { await this.executeTrigger(processId, trigger.id, { event: eventType, eventData }); } } } } stopAll(): void { this.stop(); for (const scheduled of this.scheduledTriggers.values()) { if (scheduled.interval) { const task = scheduled.interval as any; if (task.stop) task.stop(); if (task.destroy) task.destroy(); } } this.scheduledTriggers.clear(); this.activeTriggers.clear(); } async triggerProcess(processId: string, variables?: Record<string, any>): Promise<void> { const process = await this.store.getProcess(processId); if (!process) { throw new Error('Process not found'); } // Find manual trigger or use first trigger const trigger = process.triggers.find(t => t.type === 'manual') || process.triggers[0]; if (!trigger) { // Create a temporary manual trigger const manualTrigger: ProcessTrigger = { id: 'manual-temp', type: 'manual', name: 'Manual Execution', enabled: true, config: {} }; await this.engine.executeProcess(process, manualTrigger.id, variables); } else { await this.engine.executeProcess(process, trigger.id, variables); } } async enableTrigger(processId: string, triggerId: string): Promise<void> { const process = await this.store.getProcess(processId); if (!process) return; const trigger = process.triggers.find(t => t.id === triggerId); if (trigger && !trigger.enabled) { trigger.enabled = true; await this.store.saveProcess(process); await this.registerTrigger(process, trigger); } } async disableTrigger(processId: string, triggerId: string): Promise<void> { const process = await this.store.getProcess(processId); if (!process) return; const trigger = process.triggers.find(t => t.id === triggerId); if (trigger && trigger.enabled) { trigger.enabled = false; await this.store.saveProcess(process); // Remove from scheduled triggers const key = `${processId}:${triggerId}`; const scheduled = this.scheduledTriggers.get(key); if (scheduled && scheduled.interval) { clearInterval(scheduled.interval); this.scheduledTriggers.delete(key); } } } }