@casoon/auditmysite
Version:
Professional website analysis suite with robust accessibility testing, Core Web Vitals performance monitoring, SEO analysis, and content optimization insights. Features isolated browser contexts, retry mechanisms, and comprehensive API endpoints for profe
402 lines โข 14.4 kB
JavaScript
;
/**
* Batch Processing System for multiple URL audits
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.batchProcessor = exports.BatchProcessor = void 0;
const events_1 = require("events");
const accessibility_checker_main_1 = require("../../accessibility-checker-main");
const webhook_manager_1 = require("../webhooks/webhook-manager");
const performance_monitor_1 = require("../../core/monitoring/performance-monitor");
class BatchProcessor extends events_1.EventEmitter {
constructor(maxConcurrentJobs = 3) {
super();
this.jobs = new Map();
this.queue = [];
this.running = new Set();
this.maxConcurrentJobs = 3;
this.maxConcurrentJobs = maxConcurrentJobs;
this.accessibilityChecker = new accessibility_checker_main_1.MainAccessibilityChecker();
this.startJobProcessor();
}
/**
* Create a new batch job
*/
createJob(urls, options = {}, metadata) {
const jobId = `batch-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const job = {
id: jobId,
name: metadata?.name || `Batch Job ${jobId}`,
urls: [...new Set(urls)], // Remove duplicates
options: {
maxConcurrency: options.maxConcurrency || 5,
retries: options.retries || 2,
timeout: options.timeout || 120000, // 2 minutes
includeDesktopComparison: options.includeDesktopComparison || false,
formats: options.formats || ['json'],
continueOnError: options.continueOnError !== false,
priority: options.priority || 'normal',
...options
},
status: 'pending',
results: new Map(),
errors: new Map(),
progress: {
total: urls.length,
completed: 0,
failed: 0,
percentage: 0
},
metadata
};
this.jobs.set(jobId, job);
// Add to queue based on priority
if (options.priority === 'high') {
this.queue.unshift(jobId);
}
else {
this.queue.push(jobId);
}
console.log(`๐ฆ Batch job created: ${jobId} (${urls.length} URLs)`);
this.emit('jobCreated', job);
return job;
}
/**
* Start processing jobs from the queue
*/
startJobProcessor() {
setInterval(() => {
this.processQueue();
}, 1000);
console.log('๐ Batch job processor started');
}
/**
* Process jobs in the queue
*/
async processQueue() {
if (this.running.size >= this.maxConcurrentJobs || this.queue.length === 0) {
return;
}
const jobId = this.queue.shift();
if (!jobId)
return;
const job = this.jobs.get(jobId);
if (!job || job.status !== 'pending') {
return;
}
this.running.add(jobId);
try {
await this.executeJob(job);
}
catch (error) {
console.error(`โ Batch job failed: ${jobId}`, error);
}
finally {
this.running.delete(jobId);
}
}
/**
* Execute a batch job
*/
async executeJob(job) {
console.log(`๐ Starting batch job: ${job.id}`);
job.status = 'running';
job.startTime = Date.now();
this.emit('jobStarted', job);
// Trigger webhook
if (job.options.webhookId) {
await webhook_manager_1.webhookManager.trigger('batch.started', {
jobId: job.id,
urls: job.urls,
options: job.options
});
}
try {
await performance_monitor_1.performanceMonitor.monitor(`batch-job-${job.id}`, async () => {
await this.processUrls(job);
});
job.status = 'completed';
job.endTime = Date.now();
job.duration = job.endTime - (job.startTime || 0);
console.log(`โ
Batch job completed: ${job.id} (${job.duration}ms)`);
this.emit('jobCompleted', job);
// Trigger webhook
if (job.options.webhookId) {
const result = this.generateBatchResult(job);
await webhook_manager_1.webhookManager.trigger('batch.completed', {
jobId: job.id,
result
});
}
}
catch (error) {
job.status = 'failed';
job.endTime = Date.now();
job.duration = job.endTime - (job.startTime || 0);
console.error(`โ Batch job failed: ${job.id}`, error);
this.emit('jobFailed', job, error);
// Trigger webhook
if (job.options.webhookId) {
await webhook_manager_1.webhookManager.trigger('batch.failed', {
jobId: job.id,
error: error instanceof Error ? error.message : String(error)
});
}
}
}
/**
* Process URLs in a batch job
*/
async processUrls(job) {
const semaphore = new Semaphore(job.options.maxConcurrency || 5);
const promises = [];
for (const url of job.urls) {
const promise = semaphore.acquire(async () => {
await this.processUrl(job, url);
});
promises.push(promise);
}
await Promise.all(promises);
}
/**
* Process a single URL
*/
async processUrl(job, url) {
let lastError = null;
const maxRetries = job.options.retries || 2;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
console.log(`๐ Processing URL: ${url} (attempt ${attempt + 1})`);
const startTime = Date.now();
const result = await performance_monitor_1.performanceMonitor.monitor(`audit-${url}`, async () => {
return await this.accessibilityChecker.analyze('', url);
});
const duration = Date.now() - startTime;
job.results.set(url, { ...result, duration });
job.progress.completed++;
job.progress.percentage = (job.progress.completed / job.progress.total) * 100;
console.log(`โ
URL processed: ${url} (${duration}ms)`);
this.emit('urlProcessed', job, url, result);
// Trigger progress webhook
if (job.options.webhookId) {
await webhook_manager_1.webhookManager.trigger('analysis.progress', {
jobId: job.id,
url,
progress: job.progress,
result
});
}
return; // Success, exit retry loop
}
catch (error) {
lastError = error;
console.warn(`โ ๏ธ URL processing failed: ${url} (attempt ${attempt + 1})`, error?.message || String(error));
if (attempt === maxRetries) {
// Final attempt failed
job.errors.set(url, error instanceof Error ? error : new Error(String(error)));
job.progress.failed++;
job.progress.percentage = ((job.progress.completed + job.progress.failed) / job.progress.total) * 100;
console.error(`โ URL processing failed permanently: ${url}`, error?.message || String(error));
this.emit('urlFailed', job, url, error);
if (!job.options.continueOnError) {
throw error;
}
}
else {
// Wait before retry with exponential backoff
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
}
/**
* Get batch job by ID
*/
getJob(jobId) {
return this.jobs.get(jobId);
}
/**
* List all jobs
*/
listJobs(status) {
const jobs = Array.from(this.jobs.values());
return status ? jobs.filter(job => job.status === status) : jobs;
}
/**
* Cancel a batch job
*/
cancelJob(jobId) {
const job = this.jobs.get(jobId);
if (!job || job.status === 'completed') {
return false;
}
job.status = 'cancelled';
job.endTime = Date.now();
job.duration = job.endTime - (job.startTime || Date.now());
// Remove from queue if pending
const queueIndex = this.queue.indexOf(jobId);
if (queueIndex > -1) {
this.queue.splice(queueIndex, 1);
}
console.log(`๐ซ Batch job cancelled: ${jobId}`);
this.emit('jobCancelled', job);
return true;
}
/**
* Generate batch result summary
*/
generateBatchResult(job) {
const results = Array.from(job.results.entries()).map(([url, data]) => ({
url,
status: 'success',
data,
duration: data.duration
}));
const errors = Array.from(job.errors.entries()).map(([url, error]) => ({
url,
status: 'error',
error: error.message
}));
const allResults = [...results, ...errors];
// Calculate summary statistics
const successfulResults = results.filter(r => r.data);
const averageScore = successfulResults.length > 0
? successfulResults.reduce((sum, r) => sum + (r.data.qualityScore || 0), 0) / successfulResults.length
: 0;
const allIssues = successfulResults.flatMap(r => r.data.detailedIssues || []);
const totalIssues = allIssues.length;
// Find common issues
const issueCount = {};
allIssues.forEach(issue => {
const key = issue.description || issue.message;
issueCount[key] = (issueCount[key] || 0) + 1;
});
const commonIssues = Object.entries(issueCount)
.filter(([_, count]) => count >= Math.max(2, successfulResults.length * 0.3))
.map(([issue, _]) => issue)
.slice(0, 10);
// Generate recommendations based on common issues
const recommendations = this.generateRecommendations(commonIssues, allResults);
return {
jobId: job.id,
totalUrls: job.progress.total,
successfulUrls: job.progress.completed,
failedUrls: job.progress.failed,
results: allResults,
summary: {
averageScore,
totalIssues,
commonIssues,
recommendations
},
duration: job.duration || 0
};
}
/**
* Generate recommendations based on analysis results
*/
generateRecommendations(commonIssues, results) {
const recommendations = [];
if (commonIssues.some(issue => issue.includes('alt'))) {
recommendations.push('Add descriptive alt text to all images');
}
if (commonIssues.some(issue => issue.includes('heading'))) {
recommendations.push('Ensure proper heading structure (h1-h6)');
}
if (commonIssues.some(issue => issue.includes('contrast'))) {
recommendations.push('Improve color contrast for better readability');
}
if (commonIssues.some(issue => issue.includes('form') || issue.includes('label'))) {
recommendations.push('Add labels to all form inputs');
}
if (commonIssues.some(issue => issue.includes('focus'))) {
recommendations.push('Ensure all interactive elements are keyboard accessible');
}
return recommendations.slice(0, 5);
}
/**
* Get processing statistics
*/
getStats() {
const jobs = Array.from(this.jobs.values());
const completedJobs = jobs.filter(j => j.status === 'completed');
const averageProcessingTime = completedJobs.length > 0
? completedJobs.reduce((sum, job) => sum + (job.duration || 0), 0) / completedJobs.length
: 0;
return {
totalJobs: jobs.length,
runningJobs: jobs.filter(j => j.status === 'running').length,
completedJobs: completedJobs.length,
failedJobs: jobs.filter(j => j.status === 'failed').length,
queueLength: this.queue.length,
averageProcessingTime
};
}
/**
* Clean up old completed jobs
*/
cleanup(olderThanMs = 24 * 60 * 60 * 1000) {
const cutoff = Date.now() - olderThanMs;
let cleaned = 0;
for (const [jobId, job] of this.jobs.entries()) {
if ((job.status === 'completed' || job.status === 'failed') &&
job.endTime && job.endTime < cutoff) {
this.jobs.delete(jobId);
cleaned++;
}
}
console.log(`๐งน Cleaned up ${cleaned} old batch jobs`);
return cleaned;
}
/**
* Export batch processing data
*/
export() {
return {
jobs: Array.from(this.jobs.values()),
stats: this.getStats(),
timestamp: Date.now()
};
}
}
exports.BatchProcessor = BatchProcessor;
/**
* Semaphore class for controlling concurrency
*/
class Semaphore {
constructor(permits) {
this.waiting = [];
this.permits = permits;
}
async acquire(fn) {
await this.waitForPermit();
try {
return await fn();
}
finally {
this.release();
}
}
async waitForPermit() {
if (this.permits > 0) {
this.permits--;
return;
}
return new Promise((resolve) => {
this.waiting.push(resolve);
});
}
release() {
const next = this.waiting.shift();
if (next) {
next();
}
else {
this.permits++;
}
}
}
// Global batch processor instance
exports.batchProcessor = new BatchProcessor();
//# sourceMappingURL=batch-processor.js.map