@casoon/auditmysite
Version:
A comprehensive command-line tool for automated accessibility, security, performance, and SEO testing using Playwright and pa11y, based on sitemap URLs
370 lines • 14.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EventDrivenQueue = void 0;
const events_1 = require("events");
class EventDrivenQueue extends events_1.EventEmitter {
constructor(options = {}) {
super();
this.queue = [];
this.completed = [];
this.failed = [];
this.activeWorkers = new Set();
this.isProcessing = false;
this.startTime = null;
this.lastProgressUpdate = 0;
this.progressUpdateInterval = 1000; // 1 second
this.statusInterval = null;
this.options = {
maxRetries: 3,
maxConcurrent: 1,
priorityPatterns: [
{ pattern: '/home', priority: 10 },
{ pattern: '/', priority: 9 },
{ pattern: '/about', priority: 8 },
{ pattern: '/contact', priority: 7 }
],
retryDelay: 1000,
enableEvents: true,
enableShortStatus: true, // Standardmäßig aktiviert
statusUpdateInterval: 2000, // 2 Sekunden
...options
};
// Event-Listener für interne Queue-Events
if (this.options.enableEvents) {
this.setupEventListeners();
}
}
setupEventListeners() {
// Interne Event-Handler
this.on('url-added', (url, priority) => {
this.emit('queue:urlAdded', { url, priority, timestamp: new Date() });
this.options.eventCallbacks?.onUrlAdded?.(url, priority);
});
this.on('url-started', (url) => {
this.emit('queue:urlStarted', { url, timestamp: new Date() });
this.options.eventCallbacks?.onUrlStarted?.(url);
});
this.on('url-completed', (url, result, duration) => {
this.emit('queue:urlCompleted', { url, result, duration, timestamp: new Date() });
this.options.eventCallbacks?.onUrlCompleted?.(url, result, duration);
});
this.on('url-failed', (url, error, attempts) => {
this.emit('queue:urlFailed', { url, error, attempts, timestamp: new Date() });
this.options.eventCallbacks?.onUrlFailed?.(url, error, attempts);
});
this.on('url-retrying', (url, attempts) => {
this.emit('queue:urlRetrying', { url, attempts, timestamp: new Date() });
this.options.eventCallbacks?.onUrlRetrying?.(url, attempts);
});
this.on('queue-empty', () => {
this.emit('queue:empty', { timestamp: new Date() });
this.options.eventCallbacks?.onQueueEmpty?.();
});
this.on('progress-update', (stats) => {
this.emit('queue:progressUpdate', { stats, timestamp: new Date() });
this.options.eventCallbacks?.onProgressUpdate?.(stats);
});
this.on('error', (error) => {
this.emit('queue:error', { error, timestamp: new Date() });
this.options.eventCallbacks?.onError?.(error);
});
}
addUrls(urls) {
const newUrls = urls.filter(url => !this.queue.some(q => q.url === url));
newUrls.forEach(url => {
const priority = this.calculatePriority(url);
const queuedUrl = {
url,
priority,
status: 'pending',
attempts: 0
};
this.queue.push(queuedUrl);
this.emit('url-added', url, priority);
});
// Sortiere nach Priorität (höchste zuerst)
this.queue.sort((a, b) => b.priority - a.priority);
this.updateProgress();
}
getNextUrl() {
const pendingUrl = this.queue.find(q => q.status === 'pending');
if (pendingUrl && this.activeWorkers.size < this.options.maxConcurrent) {
pendingUrl.status = 'in-progress';
pendingUrl.startedAt = new Date();
pendingUrl.attempts++;
this.activeWorkers.add(pendingUrl.url);
this.emit('url-started', pendingUrl.url);
this.updateProgress();
return pendingUrl;
}
return null;
}
markCompleted(url, result) {
const queuedUrl = this.queue.find(q => q.url === url);
if (queuedUrl) {
queuedUrl.status = 'completed';
queuedUrl.result = result;
queuedUrl.completedAt = new Date();
queuedUrl.duration = queuedUrl.completedAt.getTime() - queuedUrl.startedAt.getTime();
this.completed.push(queuedUrl);
this.queue = this.queue.filter(q => q.url !== url);
this.activeWorkers.delete(url);
this.emit('url-completed', url, result, queuedUrl.duration);
this.updateProgress();
this.checkQueueEmpty();
}
}
markFailed(url, error) {
const queuedUrl = this.queue.find(q => q.url === url);
if (queuedUrl) {
if (queuedUrl.attempts < this.options.maxRetries) {
// Retry logic
queuedUrl.status = 'retrying';
this.emit('url-retrying', url, queuedUrl.attempts);
// Schedule retry
setTimeout(() => {
queuedUrl.status = 'pending';
this.updateProgress();
}, this.options.retryDelay);
}
else {
// Max retries reached
queuedUrl.status = 'failed';
queuedUrl.error = error;
queuedUrl.completedAt = new Date();
queuedUrl.duration = queuedUrl.completedAt.getTime() - queuedUrl.startedAt.getTime();
this.failed.push(queuedUrl);
this.queue = this.queue.filter(q => q.url !== url);
this.activeWorkers.delete(url);
this.emit('url-failed', url, error, queuedUrl.attempts);
this.updateProgress();
this.checkQueueEmpty();
}
}
}
checkQueueEmpty() {
if (this.queue.length === 0 && this.activeWorkers.size === 0) {
this.emit('queue-empty');
}
}
updateProgress() {
const now = Date.now();
if (now - this.lastProgressUpdate < this.progressUpdateInterval) {
return;
}
this.lastProgressUpdate = now;
const stats = this.getStats();
this.emit('progress-update', stats);
}
getStats() {
const total = this.queue.length + this.completed.length + this.failed.length;
const pending = this.queue.filter(q => q.status === 'pending').length;
const inProgress = this.queue.filter(q => q.status === 'in-progress').length;
const completed = this.completed.length;
const failed = this.failed.length;
const retrying = this.queue.filter(q => q.status === 'retrying').length;
const progress = total > 0 ? ((completed + failed) / total) * 100 : 0;
// Berechne durchschnittliche Dauer
const completedWithDuration = this.completed.filter(q => q.duration);
const averageDuration = completedWithDuration.length > 0
? completedWithDuration.reduce((sum, q) => sum + q.duration, 0) / completedWithDuration.length
: 0;
// Schätze verbleibende Zeit
const remainingItems = pending + inProgress + retrying;
const estimatedTimeRemaining = remainingItems > 0 && averageDuration > 0
? (remainingItems * averageDuration) / this.options.maxConcurrent
: 0;
// System-Metriken (vereinfacht)
const memoryUsage = process.memoryUsage().heapUsed / 1024 / 1024; // MB
const cpuUsage = process.cpuUsage().user / 1000000; // Sekunden
return {
total,
pending,
inProgress,
completed,
failed,
retrying,
progress: Math.round(progress * 100) / 100,
averageDuration: Math.round(averageDuration),
estimatedTimeRemaining: Math.round(estimatedTimeRemaining),
activeWorkers: this.activeWorkers.size,
memoryUsage: Math.round(memoryUsage * 100) / 100,
cpuUsage: Math.round(cpuUsage * 100) / 100
};
}
calculatePriority(url) {
const pattern = this.options.priorityPatterns.find(p => url.includes(p.pattern));
return pattern ? pattern.priority : 1;
}
/**
* 🚀 Integrierte Parallel-Verarbeitung der Queue
* Verarbeitet alle URLs parallel mit automatischer Status-Berichterstattung
*/
async processUrls(urls, options) {
console.log(`🚀 Starte Queue-Verarbeitung für ${urls.length} URLs mit ${this.options.maxConcurrent} Workern`);
this.addUrls(urls);
this.startTime = new Date();
// Starte Status-Updates
if (this.options.enableShortStatus) {
this.startStatusUpdates(options.onShortStatus);
}
const results = [];
const promises = [];
// Erstelle Worker-Promises
for (let i = 0; i < this.options.maxConcurrent; i++) {
promises.push(this.worker(i, options));
}
// Warte auf alle Worker
await Promise.all(promises);
// Stoppe Status-Updates
this.stopStatusUpdates();
// Sammle alle Ergebnisse
results.push(...this.completed.map(q => q.result));
const duration = Date.now() - this.startTime.getTime();
console.log(`✅ Queue-Verarbeitung abgeschlossen: ${this.completed.length}/${urls.length} URLs in ${duration}ms`);
return results;
}
/**
* 🔧 Worker-Funktion für parallele Verarbeitung
*/
async worker(workerId, options) {
while (this.queue.length > 0 || this.activeWorkers.size > 0) {
const queuedUrl = this.getNextUrl();
if (!queuedUrl) {
// Warte kurz wenn keine URLs verfügbar
await new Promise(resolve => setTimeout(resolve, 100));
continue;
}
try {
const result = await options.processor(queuedUrl.url);
this.markCompleted(queuedUrl.url, result);
options.onResult?.(queuedUrl.url, result);
}
catch (error) {
this.markFailed(queuedUrl.url, String(error));
options.onError?.(queuedUrl.url, String(error));
}
}
}
/**
* 📊 Startet kurze Status-Updates
*/
startStatusUpdates(onShortStatus) {
this.statusInterval = setInterval(() => {
const stats = this.getStats();
const status = this.generateShortStatus(stats);
this.emit('short-status', status);
onShortStatus?.(status);
this.options.eventCallbacks?.onShortStatus?.(status);
}, this.options.statusUpdateInterval);
}
/**
* ⏹️ Stoppt Status-Updates
*/
stopStatusUpdates() {
if (this.statusInterval) {
clearInterval(this.statusInterval);
this.statusInterval = null;
}
}
/**
* 📝 Generiert kurze Status-Nachricht mit verbesserter ETA
*/
generateShortStatus(stats) {
const progress = Math.round(stats.progress);
const workers = `${stats.activeWorkers}/${this.options.maxConcurrent}`;
const memory = Math.round(stats.memoryUsage);
// Verbesserte ETA-Berechnung
let eta = '?';
if (stats.estimatedTimeRemaining > 0) {
const seconds = Math.round(stats.estimatedTimeRemaining / 1000);
if (seconds < 60) {
eta = `${seconds}s`;
}
else if (seconds < 3600) {
eta = `${Math.round(seconds / 60)}m`;
}
else {
eta = `${Math.round(seconds / 3600)}h`;
}
}
// Speed-Indicator (URLs pro Minute)
const speed = stats.completed > 0 && this.startTime
? Math.round((stats.completed / ((Date.now() - this.startTime.getTime()) / 60000)) * 10) / 10
: 0;
// Memory-Warning
const memoryWarning = stats.memoryUsage > 500 ? ' ⚠️' : '';
return `📊 ${progress}% | ${stats.completed}/${stats.total} | 🔧 ${workers} | 💾 ${memory}MB${memoryWarning} | ⏱️ ${eta} | 🚀 ${speed}/min`;
}
// Public API für Event-Listener
onUrlAdded(callback) {
this.on('queue:urlAdded', callback);
return this;
}
onUrlStarted(callback) {
this.on('queue:urlStarted', callback);
return this;
}
onUrlCompleted(callback) {
this.on('queue:urlCompleted', callback);
return this;
}
onUrlFailed(callback) {
this.on('queue:urlFailed', callback);
return this;
}
onUrlRetrying(callback) {
this.on('queue:urlRetrying', callback);
return this;
}
onQueueEmpty(callback) {
this.on('queue:empty', callback);
return this;
}
onProgressUpdate(callback) {
this.on('queue:progressUpdate', callback);
return this;
}
onError(callback) {
this.on('queue:error', callback);
return this;
}
// Utility-Methoden
getCompletedResults() {
return this.completed.map(q => q.result);
}
getFailedResults() {
return this.failed.map(q => ({ url: q.url, error: q.error, attempts: q.attempts }));
}
clear() {
this.queue = [];
this.completed = [];
this.failed = [];
this.activeWorkers.clear();
this.isProcessing = false;
this.startTime = null;
}
pause() {
this.isProcessing = false;
}
resume() {
this.isProcessing = true;
}
isPaused() {
return !this.isProcessing;
}
getQueueSize() {
return this.queue.length;
}
getActiveWorkers() {
return this.activeWorkers.size;
}
getMaxConcurrent() {
return this.options.maxConcurrent;
}
setMaxConcurrent(max) {
this.options.maxConcurrent = max;
}
}
exports.EventDrivenQueue = EventDrivenQueue;
//# sourceMappingURL=event-driven-queue.js.map