UNPKG

adpa-enterprise-framework-automation

Version:

Modular, standards-compliant Node.js/TypeScript automation framework for enterprise requirements, project, and data management. Provides CLI and API for BABOK v3, PMBOK 7th Edition, and DMBOK 2.0 (in progress). Production-ready Express.js API with TypeSpe

279 lines 9.95 kB
import { ProcessingStatus } from '../types/api.js'; /** * Job Management Service * * Handles job storage, retrieval, and lifecycle management * for document conversion operations. */ export class JobManager { jobs = new Map(); batches = new Map(); /** * Get job by ID */ async getJob(jobId) { return this.jobs.get(jobId) || null; } /** * Get batch by ID */ async getBatch(batchId) { return this.batches.get(batchId) || null; } /** * Store job */ async storeJob(job) { this.jobs.set(job.id, job); } /** * Store batch */ async storeBatch(batch) { this.batches.set(batch.id, batch); } /** * Cancel a job */ async cancelJob(jobId) { const job = this.jobs.get(jobId); if (!job) { return { success: false, message: `Job with ID ${jobId} not found`, statusCode: 404, errorCode: 'RESOURCE_NOT_FOUND' }; } if (job.status === ProcessingStatus.completed) { return { success: false, message: 'Cannot cancel completed job', statusCode: 409, errorCode: 'RESOURCE_CONFLICT' }; } if (job.status === ProcessingStatus.cancelled) { return { success: false, message: 'Job is already cancelled', statusCode: 409, errorCode: 'RESOURCE_CONFLICT' }; } job.updateStatus(ProcessingStatus.cancelled); return { success: true, message: 'Job cancelled successfully' }; } /** * List jobs with filtering and pagination */ async listJobs(params) { let filteredJobs = Array.from(this.jobs.values()); // Apply filters if (params.filters.status) { filteredJobs = filteredJobs.filter(job => job.status === params.filters.status); } if (params.filters.fromDate) { filteredJobs = filteredJobs.filter(job => job.createdAt >= params.filters.fromDate); } if (params.filters.toDate) { filteredJobs = filteredJobs.filter(job => job.createdAt <= params.filters.toDate); } if (params.filters.outputFormat) { filteredJobs = filteredJobs.filter(job => job.request.outputFormat === params.filters.outputFormat); } if (params.filters.search) { const searchTerm = params.filters.search.toLowerCase(); filteredJobs = filteredJobs.filter(job => job.generatedFilename?.toLowerCase().includes(searchTerm) || job.originalFilename?.toLowerCase().includes(searchTerm) || job.request.metadata?.title?.toLowerCase().includes(searchTerm)); } // Apply sorting filteredJobs.sort((a, b) => { let aValue = a[params.sort]; let bValue = b[params.sort]; if (aValue instanceof Date) aValue = aValue.getTime(); if (bValue instanceof Date) bValue = bValue.getTime(); if (params.order === 'asc') { return aValue > bValue ? 1 : -1; } else { return aValue < bValue ? 1 : -1; } }); // Apply pagination const total = filteredJobs.length; const totalPages = Math.ceil(total / params.limit); const offset = (params.page - 1) * params.limit; const paginatedJobs = filteredJobs.slice(offset, offset + params.limit); // Convert to response format const items = paginatedJobs.map(job => ({ jobId: job.id, status: job.status, downloadUrl: job.downloadUrl, fileSize: job.fileSize, progress: job.progress, estimatedCompletion: job.estimatedCompletion, createdAt: job.createdAt, completedAt: job.completedAt, processingDuration: job.processingDuration, outputFormat: job.request.outputFormat, originalFilename: job.originalFilename, generatedFilename: job.generatedFilename, error: job.error, logs: job.logs })); return { items, pagination: { page: params.page, limit: params.limit, total, totalPages, hasNext: params.page < totalPages, hasPrev: params.page > 1 } }; } /** * Get conversion statistics */ async getStatistics(params) { let jobs = Array.from(this.jobs.values()); // Apply date filters if (params.fromDate) { jobs = jobs.filter(job => job.createdAt >= params.fromDate); } if (params.toDate) { jobs = jobs.filter(job => job.createdAt <= params.toDate); } const totalJobs = jobs.length; const successfulJobs = jobs.filter(job => job.status === ProcessingStatus.completed).length; const failedJobs = jobs.filter(job => job.status === ProcessingStatus.failed).length; const successRate = totalJobs > 0 ? (successfulJobs / totalJobs) * 100 : 0; // Calculate processing time statistics const completedJobs = jobs.filter(job => job.processingDuration); const processingTimes = completedJobs.map(job => job.processingDuration); const processingTime = { average: processingTimes.length > 0 ? Math.round(processingTimes.reduce((a, b) => a + b, 0) / processingTimes.length) : 0, min: processingTimes.length > 0 ? Math.min(...processingTimes) : 0, max: processingTimes.length > 0 ? Math.max(...processingTimes) : 0, median: processingTimes.length > 0 ? this.calculateMedian(processingTimes) : 0 }; // Format breakdown const inputFormatBreakdown = {}; const outputFormatBreakdown = {}; const errorBreakdown = {}; jobs.forEach(job => { // Input format breakdown const inputFormat = job.request.inputFormat; inputFormatBreakdown[inputFormat] = (inputFormatBreakdown[inputFormat] || 0) + 1; // Output format breakdown const outputFormat = job.request.outputFormat; outputFormatBreakdown[outputFormat] = (outputFormatBreakdown[outputFormat] || 0) + 1; // Error breakdown if (job.error) { const errorCode = job.error.code; errorBreakdown[errorCode] = (errorBreakdown[errorCode] || 0) + 1; } }); return { totalJobs, successfulJobs, failedJobs, successRate, processingTime, inputFormatBreakdown, outputFormatBreakdown, errorBreakdown, generatedAt: new Date() }; } /** * Retry a failed job */ async retryJob(jobId, options) { const job = this.jobs.get(jobId); if (!job) { return { success: false, message: `Job with ID ${jobId} not found`, statusCode: 404, errorCode: 'RESOURCE_NOT_FOUND' }; } if (job.status !== ProcessingStatus.failed) { return { success: false, message: 'Only failed jobs can be retried', statusCode: 409, errorCode: 'RESOURCE_CONFLICT' }; } // Reset job state for retry job.updateStatus(ProcessingStatus.queued); job.progress = 0; job.error = undefined; job.downloadUrl = undefined; job.completedAt = undefined; job.processingDuration = undefined; // Apply any updated options if (options) { job.request.options = { ...job.request.options, ...options }; } return { success: true, message: 'Job queued for retry', job: { jobId: job.id, status: job.status, progress: job.progress, createdAt: job.createdAt, outputFormat: job.request.outputFormat } }; } /** * Get file data for download */ async getJobFile(jobId) { const job = this.jobs.get(jobId); if (!job) { throw new Error(`Job with ID ${jobId} not found`); } if (job.status !== ProcessingStatus.completed) { throw new Error('Document is not ready for download'); } // Mock file content generation const content = Buffer.from(`Mock ${job.request.outputFormat} content for job ${jobId}`); const contentTypeMap = { pdf: 'application/pdf', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', html: 'text/html', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }; return { content, contentType: contentTypeMap[job.request.outputFormat] || 'application/octet-stream', filename: job.generatedFilename || `document.${job.request.outputFormat}`, size: content.length }; } /** * Calculate median from array of numbers */ calculateMedian(numbers) { const sorted = numbers.slice().sort((a, b) => a - b); const middle = Math.floor(sorted.length / 2); if (sorted.length % 2 === 0) { return Math.round((sorted[middle - 1] + sorted[middle]) / 2); } return sorted[middle]; } } //# sourceMappingURL=JobManager.js.map