UNPKG

shipdeck

Version:

Ship MVPs in 48 hours. Fix bugs in 30 seconds. The command deck for developers who ship.

590 lines (514 loc) 16.1 kB
/** * Real-time Progress Tracker for DAG Workflows * Provides detailed progress monitoring, metrics, and real-time updates */ const EventEmitter = require('events'); /** * Progress tracking for individual workflow nodes */ class NodeProgressTracker { constructor(nodeId, nodeName) { this.nodeId = nodeId; this.nodeName = nodeName; this.status = 'pending'; this.startedAt = null; this.completedAt = null; this.duration = null; this.attempts = 0; this.progress = 0; // 0-100 this.estimatedCompletion = null; this.metrics = { tokensUsed: 0, cost: 0, errors: [] }; } /** * Update node progress */ updateProgress(status, progress = 0, metadata = {}) { this.status = status; this.progress = Math.max(0, Math.min(100, progress)); if (status === 'running' && !this.startedAt) { this.startedAt = new Date(); this.estimatedCompletion = this.calculateEstimatedCompletion(metadata.timeout); } if (['completed', 'failed', 'skipped'].includes(status)) { this.completedAt = new Date(); this.duration = this.startedAt ? this.completedAt - this.startedAt : 0; this.progress = status === 'completed' ? 100 : this.progress; } if (metadata.attempts) this.attempts = metadata.attempts; if (metadata.tokensUsed) this.metrics.tokensUsed = metadata.tokensUsed; if (metadata.cost) this.metrics.cost = metadata.cost; if (metadata.error) this.metrics.errors.push(metadata.error); } /** * Calculate estimated completion time */ calculateEstimatedCompletion(timeoutMs = 300000) { if (!this.startedAt) return null; // Use timeout as base estimate, adjust based on progress const baseEstimate = timeoutMs; const adjustedEstimate = this.progress > 0 ? (baseEstimate * (100 - this.progress)) / 100 : baseEstimate; return new Date(this.startedAt.getTime() + adjustedEstimate); } /** * Get progress summary */ getSummary() { return { nodeId: this.nodeId, name: this.nodeName, status: this.status, progress: this.progress, duration: this.duration, attempts: this.attempts, startedAt: this.startedAt, completedAt: this.completedAt, estimatedCompletion: this.estimatedCompletion, metrics: { ...this.metrics } }; } } /** * Main workflow progress tracker */ class WorkflowProgressTracker extends EventEmitter { constructor(workflowId, workflowName, totalNodes) { super(); this.workflowId = workflowId; this.workflowName = workflowName; this.totalNodes = totalNodes; this.nodeTrackers = new Map(); this.startedAt = null; this.completedAt = null; this.status = 'pending'; // Progress metrics this.completedNodes = 0; this.failedNodes = 0; this.skippedNodes = 0; this.runningNodes = 0; this.overallProgress = 0; this.estimatedCompletion = null; // Performance metrics this.metrics = { totalDuration: 0, averageNodeDuration: 0, parallelizationEfficiency: 0, totalTokensUsed: 0, totalCost: 0, errorRate: 0, throughput: 0 // nodes per minute }; // Real-time updates this.updateInterval = null; this.lastUpdateTime = new Date(); this.progressHistory = []; } /** * Initialize node trackers */ initializeNodes(nodeConfigs) { for (const nodeConfig of nodeConfigs) { const tracker = new NodeProgressTracker(nodeConfig.id, nodeConfig.name); this.nodeTrackers.set(nodeConfig.id, tracker); } } /** * Start workflow tracking */ start() { this.startedAt = new Date(); this.status = 'running'; this.lastUpdateTime = new Date(); // Start real-time updates this.updateInterval = setInterval(() => { this.updateMetrics(); this.emitProgressUpdate(); }, 5000); // Update every 5 seconds this.emit('workflow:started', { workflowId: this.workflowId, name: this.workflowName, totalNodes: this.totalNodes, startedAt: this.startedAt }); } /** * Update node progress */ updateNodeProgress(nodeId, status, progress = 0, metadata = {}) { const tracker = this.nodeTrackers.get(nodeId); if (!tracker) { console.warn(`Node tracker not found: ${nodeId}`); return; } const previousStatus = tracker.status; tracker.updateProgress(status, progress, metadata); // Update counters when status changes if (previousStatus !== status) { this.updateCounters(previousStatus, status); } // Update overall progress this.calculateOverallProgress(); this.updateMetrics(); // Emit node update this.emit('node:updated', { workflowId: this.workflowId, nodeId, status, progress, summary: tracker.getSummary() }); // Check if workflow is complete if (this.isWorkflowComplete()) { this.complete(); } } /** * Update node counters */ updateCounters(oldStatus, newStatus) { // Decrement old status if (oldStatus === 'running') this.runningNodes--; // Increment new status if (newStatus === 'running') this.runningNodes++; else if (newStatus === 'completed') this.completedNodes++; else if (newStatus === 'failed') this.failedNodes++; else if (newStatus === 'skipped') this.skippedNodes++; } /** * Calculate overall workflow progress */ calculateOverallProgress() { if (this.totalNodes === 0) { this.overallProgress = 0; return; } let totalProgress = 0; for (const tracker of this.nodeTrackers.values()) { totalProgress += tracker.progress; } this.overallProgress = Math.round(totalProgress / this.totalNodes); // Update estimated completion this.calculateEstimatedCompletion(); } /** * Calculate estimated workflow completion time */ calculateEstimatedCompletion() { if (!this.startedAt || this.overallProgress === 0) { this.estimatedCompletion = null; return; } const elapsed = new Date() - this.startedAt; const progressRate = this.overallProgress / elapsed; // progress per ms const remainingProgress = 100 - this.overallProgress; const estimatedRemaining = remainingProgress / progressRate; this.estimatedCompletion = new Date(Date.now() + estimatedRemaining); } /** * Update performance metrics */ updateMetrics() { const now = new Date(); const elapsed = now - this.startedAt; // Calculate averages const completedTrackers = Array.from(this.nodeTrackers.values()) .filter(t => t.completedAt); if (completedTrackers.length > 0) { const totalDuration = completedTrackers.reduce((sum, t) => sum + t.duration, 0); this.metrics.averageNodeDuration = totalDuration / completedTrackers.length; } // Calculate throughput this.metrics.throughput = this.completedNodes / (elapsed / 60000); // nodes per minute // Calculate parallelization efficiency const theoreticalSequentialTime = this.metrics.averageNodeDuration * this.totalNodes; this.metrics.parallelizationEfficiency = theoreticalSequentialTime > 0 ? (theoreticalSequentialTime / elapsed) * 100 : 0; // Aggregate costs and tokens this.metrics.totalTokensUsed = Array.from(this.nodeTrackers.values()) .reduce((sum, t) => sum + t.metrics.tokensUsed, 0); this.metrics.totalCost = Array.from(this.nodeTrackers.values()) .reduce((sum, t) => sum + t.metrics.cost, 0); // Calculate error rate const totalErrors = Array.from(this.nodeTrackers.values()) .reduce((sum, t) => sum + t.metrics.errors.length, 0); this.metrics.errorRate = (totalErrors / this.totalNodes) * 100; // Store progress snapshot this.progressHistory.push({ timestamp: now, overallProgress: this.overallProgress, runningNodes: this.runningNodes, completedNodes: this.completedNodes, failedNodes: this.failedNodes }); // Keep only last 100 snapshots if (this.progressHistory.length > 100) { this.progressHistory = this.progressHistory.slice(-100); } } /** * Check if workflow is complete */ isWorkflowComplete() { return (this.completedNodes + this.failedNodes + this.skippedNodes) >= this.totalNodes; } /** * Mark workflow as complete */ complete(finalStatus = null) { if (this.status !== 'running') return; this.completedAt = new Date(); this.metrics.totalDuration = this.completedAt - this.startedAt; // Determine final status if (finalStatus) { this.status = finalStatus; } else if (this.failedNodes > 0) { this.status = 'failed'; } else if (this.completedNodes === this.totalNodes) { this.status = 'completed'; } else { this.status = 'partial'; } this.overallProgress = 100; // Stop real-time updates if (this.updateInterval) { clearInterval(this.updateInterval); this.updateInterval = null; } // Final metrics update this.updateMetrics(); this.emit('workflow:completed', { workflowId: this.workflowId, status: this.status, duration: this.metrics.totalDuration, metrics: { ...this.metrics }, summary: this.getSummary() }); } /** * Cancel workflow tracking */ cancel() { this.status = 'cancelled'; this.complete('cancelled'); } /** * Emit progress update */ emitProgressUpdate() { this.emit('progress:update', { workflowId: this.workflowId, overallProgress: this.overallProgress, status: this.status, nodes: { total: this.totalNodes, completed: this.completedNodes, failed: this.failedNodes, running: this.runningNodes, pending: this.totalNodes - this.completedNodes - this.failedNodes - this.skippedNodes - this.runningNodes }, estimatedCompletion: this.estimatedCompletion, metrics: { ...this.metrics } }); } /** * Get detailed progress summary */ getSummary() { return { workflowId: this.workflowId, name: this.workflowName, status: this.status, overallProgress: this.overallProgress, startedAt: this.startedAt, completedAt: this.completedAt, estimatedCompletion: this.estimatedCompletion, nodes: { total: this.totalNodes, completed: this.completedNodes, failed: this.failedNodes, skipped: this.skippedNodes, running: this.runningNodes, pending: this.totalNodes - this.completedNodes - this.failedNodes - this.skippedNodes - this.runningNodes }, metrics: { ...this.metrics }, nodeDetails: Array.from(this.nodeTrackers.values()).map(t => t.getSummary()), progressHistory: [...this.progressHistory] }; } /** * Get real-time progress for display */ getProgressDisplay() { return { workflowId: this.workflowId, name: this.workflowName, progress: this.overallProgress, status: this.status, nodesCompleted: this.completedNodes, totalNodes: this.totalNodes, runningNodes: this.runningNodes, failedNodes: this.failedNodes, estimatedCompletion: this.estimatedCompletion, duration: this.startedAt ? new Date() - this.startedAt : 0, throughput: this.metrics.throughput, cost: this.metrics.totalCost }; } /** * Get progress chart data */ getChartData() { return { progressOverTime: this.progressHistory.map(h => ({ timestamp: h.timestamp, progress: h.overallProgress })), nodeStatusDistribution: { completed: this.completedNodes, failed: this.failedNodes, running: this.runningNodes, pending: this.totalNodes - this.completedNodes - this.failedNodes - this.skippedNodes - this.runningNodes, skipped: this.skippedNodes }, performanceMetrics: { throughput: this.metrics.throughput, parallelizationEfficiency: this.metrics.parallelizationEfficiency, errorRate: this.metrics.errorRate, averageNodeDuration: this.metrics.averageNodeDuration } }; } /** * Export progress data */ exportData() { return { workflow: this.getSummary(), chartData: this.getChartData(), exportedAt: new Date() }; } /** * Cleanup resources */ cleanup() { if (this.updateInterval) { clearInterval(this.updateInterval); this.updateInterval = null; } this.removeAllListeners(); this.nodeTrackers.clear(); this.progressHistory = []; } } /** * Progress tracker manager for multiple workflows */ class ProgressTrackerManager extends EventEmitter { constructor() { super(); this.trackers = new Map(); this.globalMetrics = { totalWorkflows: 0, activeWorkflows: 0, completedWorkflows: 0, failedWorkflows: 0, totalNodes: 0, totalCost: 0, averageCompletionTime: 0 }; } /** * Create workflow tracker */ createTracker(workflowId, workflowName, totalNodes) { if (this.trackers.has(workflowId)) { throw new Error(`Tracker already exists: ${workflowId}`); } const tracker = new WorkflowProgressTracker(workflowId, workflowName, totalNodes); // Forward events tracker.on('workflow:started', (data) => this.emit('workflow:started', data)); tracker.on('workflow:completed', (data) => { this.emit('workflow:completed', data); this.updateGlobalMetrics(); }); tracker.on('node:updated', (data) => this.emit('node:updated', data)); tracker.on('progress:update', (data) => this.emit('progress:update', data)); this.trackers.set(workflowId, tracker); this.updateGlobalMetrics(); return tracker; } /** * Get workflow tracker */ getTracker(workflowId) { return this.trackers.get(workflowId); } /** * Remove workflow tracker */ removeTracker(workflowId) { const tracker = this.trackers.get(workflowId); if (tracker) { tracker.cleanup(); this.trackers.delete(workflowId); this.updateGlobalMetrics(); } } /** * Update global metrics */ updateGlobalMetrics() { const trackers = Array.from(this.trackers.values()); this.globalMetrics.totalWorkflows = trackers.length; this.globalMetrics.activeWorkflows = trackers.filter(t => t.status === 'running').length; this.globalMetrics.completedWorkflows = trackers.filter(t => t.status === 'completed').length; this.globalMetrics.failedWorkflows = trackers.filter(t => t.status === 'failed').length; this.globalMetrics.totalNodes = trackers.reduce((sum, t) => sum + t.totalNodes, 0); this.globalMetrics.totalCost = trackers.reduce((sum, t) => sum + t.metrics.totalCost, 0); const completedTrackers = trackers.filter(t => t.completedAt && t.startedAt); if (completedTrackers.length > 0) { const totalDuration = completedTrackers.reduce((sum, t) => sum + (t.completedAt - t.startedAt), 0); this.globalMetrics.averageCompletionTime = totalDuration / completedTrackers.length; } } /** * Get all trackers */ getAllTrackers() { return Array.from(this.trackers.values()); } /** * Get global progress overview */ getGlobalOverview() { return { metrics: { ...this.globalMetrics }, activeWorkflows: this.getAllTrackers() .filter(t => t.status === 'running') .map(t => t.getProgressDisplay()) }; } /** * Cleanup all trackers */ cleanup() { for (const tracker of this.trackers.values()) { tracker.cleanup(); } this.trackers.clear(); this.removeAllListeners(); } } module.exports = { WorkflowProgressTracker, NodeProgressTracker, ProgressTrackerManager };