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