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
JavaScript
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