UNPKG

okta-mcp-server

Version:

Model Context Protocol (MCP) server for Okta API operations with support for bulk operations and caching

214 lines 7.1 kB
import { EventEmitter } from 'events'; import { randomUUID } from 'crypto'; import { logger } from '../../utils/logger.js'; export class ProgressTracker extends EventEmitter { operations = new Map(); updateInterval = null; constructor() { super(); this.startUpdateInterval(); } /** * Create a new bulk operation */ createOperation(type, total) { const operationId = randomUUID(); const operation = { operationId, type, status: 'pending', total, processed: 0, succeeded: 0, failed: 0, startTime: new Date().toISOString(), errors: [], }; this.operations.set(operationId, operation); this.emit('operation:created', operation); logger.info('Bulk operation created', { operationId, type, total, }); return operationId; } /** * Start a bulk operation */ startOperation(operationId, totalBatches) { const operation = this.operations.get(operationId); if (!operation) { throw new Error(`Operation ${operationId} not found`); } operation.status = 'running'; if (totalBatches) { operation.totalBatches = totalBatches; operation.currentBatch = 0; } this.emit('operation:started', operation); logger.info('Bulk operation started', { operationId }); } /** * Update operation progress */ updateProgress(operationId, update) { const operation = this.operations.get(operationId); if (!operation) { throw new Error(`Operation ${operationId} not found`); } if (update.processed !== undefined) { operation.processed = update.processed; } if (update.succeeded !== undefined) { operation.succeeded = update.succeeded; } if (update.failed !== undefined) { operation.failed = update.failed; } if (update.currentBatch !== undefined) { operation.currentBatch = update.currentBatch; } // Calculate estimated time remaining if (operation.processed > 0) { const elapsedMs = Date.now() - new Date(operation.startTime).getTime(); const avgTimePerItem = elapsedMs / operation.processed; const remainingItems = operation.total - operation.processed; operation.estimatedTimeRemaining = Math.round(avgTimePerItem * remainingItems); } this.emit('operation:progress', operation); } /** * Increment progress counters */ incrementProgress(operationId, succeeded, error) { const operation = this.operations.get(operationId); if (!operation) { throw new Error(`Operation ${operationId} not found`); } operation.processed++; if (succeeded) { operation.succeeded++; } else { operation.failed++; if (error) { operation.errors.push({ index: operation.processed - 1, ...error, }); } } // Calculate estimated time remaining const elapsedMs = Date.now() - new Date(operation.startTime).getTime(); const avgTimePerItem = elapsedMs / operation.processed; const remainingItems = operation.total - operation.processed; operation.estimatedTimeRemaining = Math.round(avgTimePerItem * remainingItems); this.emit('operation:progress', operation); } /** * Add an error to the operation */ addError(operationId, error) { const operation = this.operations.get(operationId); if (!operation) { throw new Error(`Operation ${operationId} not found`); } operation.errors.push(error); this.emit('operation:error', { operation, error }); } /** * Complete a bulk operation */ completeOperation(operationId, results) { const operation = this.operations.get(operationId); if (!operation) { throw new Error(`Operation ${operationId} not found`); } operation.status = operation.failed > 0 && operation.succeeded === 0 ? 'failed' : 'completed'; operation.endTime = new Date().toISOString(); if (results) { operation.results = results; } this.emit('operation:completed', operation); logger.info('Bulk operation completed', { operationId, status: operation.status, succeeded: operation.succeeded, failed: operation.failed, duration: Date.now() - new Date(operation.startTime).getTime(), }); } /** * Cancel a bulk operation */ cancelOperation(operationId) { const operation = this.operations.get(operationId); if (!operation) { throw new Error(`Operation ${operationId} not found`); } operation.status = 'cancelled'; operation.endTime = new Date().toISOString(); this.emit('operation:cancelled', operation); logger.info('Bulk operation cancelled', { operationId }); } /** * Get operation status */ getOperation(operationId) { return this.operations.get(operationId); } /** * Get all operations */ getAllOperations() { return Array.from(this.operations.values()); } /** * Clean up old operations */ cleanupOldOperations(maxAgeMs = 24 * 60 * 60 * 1000) { const now = Date.now(); const toDelete = []; this.operations.forEach((operation, id) => { if (operation.endTime) { const age = now - new Date(operation.endTime).getTime(); if (age > maxAgeMs) { toDelete.push(id); } } }); toDelete.forEach((id) => { this.operations.delete(id); logger.debug('Cleaned up old operation', { operationId: id }); }); } /** * Start periodic update interval */ startUpdateInterval() { this.updateInterval = setInterval(() => { // Emit updates for all running operations this.operations.forEach((operation) => { if (operation.status === 'running') { this.emit('operation:update', operation); } }); // Clean up old operations this.cleanupOldOperations(); }, 5000); // Update every 5 seconds } /** * Stop the progress tracker */ stop() { if (this.updateInterval) { clearInterval(this.updateInterval); this.updateInterval = null; } this.removeAllListeners(); } } // Singleton instance export const progressTracker = new ProgressTracker(); //# sourceMappingURL=progress-tracker.js.map