Files
michaelschiemer/resources/js/modules/api-manager/WorkerManager.js
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

648 lines
25 KiB
JavaScript

// modules/api-manager/WorkerManager.js
import { Logger } from '../../core/logger.js';
/**
* Worker APIs Manager - Web Workers, Service Workers, Shared Workers
*/
export class WorkerManager {
constructor(config = {}) {
this.config = config;
this.activeWorkers = new Map();
this.messageHandlers = new Map();
this.workerScripts = new Map();
// Check API support
this.support = {
webWorkers: 'Worker' in window,
serviceWorker: 'serviceWorker' in navigator,
sharedWorker: 'SharedWorker' in window,
offscreenCanvas: 'OffscreenCanvas' in window
};
Logger.info('[WorkerManager] Initialized with support:', this.support);
}
/**
* Web Workers for background processing
*/
web = {
// Create a new Web Worker
create: (script, options = {}) => {
if (!this.support.webWorkers) {
throw new Error('Web Workers not supported');
}
let worker;
const workerId = this.generateId('worker');
try {
// Handle different script types
if (typeof script === 'string') {
// URL string
worker = new Worker(script, options);
} else if (typeof script === 'function') {
// Function to blob
const blob = this.createWorkerBlob(script);
worker = new Worker(URL.createObjectURL(blob), options);
} else if (script instanceof Blob) {
// Blob directly
worker = new Worker(URL.createObjectURL(script), options);
} else {
throw new Error('Invalid script type');
}
// Enhanced worker wrapper
const workerWrapper = {
id: workerId,
worker,
// Send message to worker
send: (message, transfer = null) => {
if (transfer) {
worker.postMessage(message, transfer);
} else {
worker.postMessage(message);
}
Logger.info(`[WorkerManager] Message sent to worker: ${workerId}`);
},
// Listen for messages
onMessage: (callback) => {
worker.addEventListener('message', (event) => {
callback(event.data, event);
});
},
// Handle errors
onError: (callback) => {
worker.addEventListener('error', callback);
worker.addEventListener('messageerror', callback);
},
// Terminate worker
terminate: () => {
worker.terminate();
this.activeWorkers.delete(workerId);
Logger.info(`[WorkerManager] Worker terminated: ${workerId}`);
},
// Execute function in worker
execute: (fn, data = null) => {
return new Promise((resolve, reject) => {
const messageId = this.generateId('msg');
const handler = (event) => {
if (event.data.id === messageId) {
worker.removeEventListener('message', handler);
if (event.data.error) {
reject(new Error(event.data.error));
} else {
resolve(event.data.result);
}
}
};
worker.addEventListener('message', handler);
worker.postMessage({
id: messageId,
type: 'execute',
function: fn.toString(),
data
});
// Timeout after 30 seconds
setTimeout(() => {
worker.removeEventListener('message', handler);
reject(new Error('Worker execution timeout'));
}, 30000);
});
}
};
this.activeWorkers.set(workerId, workerWrapper);
Logger.info(`[WorkerManager] Web Worker created: ${workerId}`);
return workerWrapper;
} catch (error) {
Logger.error('[WorkerManager] Worker creation failed:', error);
throw error;
}
},
// Create worker pool for parallel processing
createPool: (script, poolSize = navigator.hardwareConcurrency || 4, options = {}) => {
const workers = [];
for (let i = 0; i < poolSize; i++) {
workers.push(this.web.create(script, options));
}
let currentWorker = 0;
const pool = {
workers,
// Execute task on next available worker
execute: async (fn, data = null) => {
const worker = workers[currentWorker];
currentWorker = (currentWorker + 1) % workers.length;
return worker.execute(fn, data);
},
// Broadcast message to all workers
broadcast: (message) => {
workers.forEach(worker => {
worker.send(message);
});
},
// Terminate all workers
terminate: () => {
workers.forEach(worker => {
worker.terminate();
});
workers.length = 0;
Logger.info('[WorkerManager] Worker pool terminated');
}
};
Logger.info(`[WorkerManager] Worker pool created with ${poolSize} workers`);
return pool;
},
// Common worker tasks
tasks: {
// Heavy computation
compute: (fn, data) => {
const workerCode = `
self.addEventListener('message', function(e) {
const { id, function: fnString, data } = e.data;
try {
const fn = new Function('return ' + fnString)();
const result = fn(data);
self.postMessage({ id, result });
} catch (error) {
self.postMessage({ id, error: error.message });
}
});
`;
const worker = this.web.create(workerCode);
return worker.execute(fn, data);
},
// Image processing
processImage: (imageData, filters) => {
const workerCode = `
self.addEventListener('message', function(e) {
const { id, data: { imageData, filters } } = e.data;
try {
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
// Apply filters
if (filters.brightness) {
pixels[i] *= filters.brightness; // R
pixels[i + 1] *= filters.brightness; // G
pixels[i + 2] *= filters.brightness; // B
}
if (filters.contrast) {
const factor = (259 * (filters.contrast * 255 + 255)) / (255 * (259 - filters.contrast * 255));
pixels[i] = factor * (pixels[i] - 128) + 128;
pixels[i + 1] = factor * (pixels[i + 1] - 128) + 128;
pixels[i + 2] = factor * (pixels[i + 2] - 128) + 128;
}
if (filters.grayscale) {
const gray = 0.299 * pixels[i] + 0.587 * pixels[i + 1] + 0.114 * pixels[i + 2];
pixels[i] = gray;
pixels[i + 1] = gray;
pixels[i + 2] = gray;
}
}
self.postMessage({ id, result: imageData }, [imageData.data.buffer]);
} catch (error) {
self.postMessage({ id, error: error.message });
}
});
`;
const worker = this.web.create(workerCode);
return worker.execute(null, { imageData, filters });
},
// Data processing
processData: (data, processor) => {
const workerCode = `
self.addEventListener('message', function(e) {
const { id, data, function: processorString } = e.data;
try {
const processor = new Function('return ' + processorString)();
const result = data.map(processor);
self.postMessage({ id, result });
} catch (error) {
self.postMessage({ id, error: error.message });
}
});
`;
const worker = this.web.create(workerCode);
return worker.execute(processor, data);
}
}
};
/**
* Service Workers for caching and background sync
*/
service = {
// Register service worker
register: async (scriptURL, options = {}) => {
if (!this.support.serviceWorker) {
throw new Error('Service Worker not supported');
}
try {
const registration = await navigator.serviceWorker.register(scriptURL, options);
Logger.info('[WorkerManager] Service Worker registered:', scriptURL);
// Enhanced registration wrapper
return {
registration,
scope: registration.scope,
// Update service worker
update: () => registration.update(),
// Unregister service worker
unregister: () => registration.unregister(),
// Send message to service worker
postMessage: (message) => {
if (registration.active) {
registration.active.postMessage(message);
}
},
// Listen for updates
onUpdate: (callback) => {
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
callback(newWorker);
}
});
});
},
// Check for updates
checkForUpdates: () => {
return registration.update();
}
};
} catch (error) {
Logger.error('[WorkerManager] Service Worker registration failed:', error);
throw error;
}
},
// Get registration
getRegistration: async (scope = '/') => {
if (!this.support.serviceWorker) {
return null;
}
try {
return await navigator.serviceWorker.getRegistration(scope);
} catch (error) {
Logger.error('[WorkerManager] Get registration failed:', error);
return null;
}
},
// Get all registrations
getRegistrations: async () => {
if (!this.support.serviceWorker) {
return [];
}
try {
return await navigator.serviceWorker.getRegistrations();
} catch (error) {
Logger.error('[WorkerManager] Get registrations failed:', error);
return [];
}
}
};
/**
* Shared Workers for cross-tab communication
*/
shared = {
// Create shared worker
create: (script, options = {}) => {
if (!this.support.sharedWorker) {
throw new Error('Shared Worker not supported');
}
try {
const sharedWorker = new SharedWorker(script, options);
const port = sharedWorker.port;
const workerId = this.generateId('shared');
// Start the port
port.start();
const workerWrapper = {
id: workerId,
worker: sharedWorker,
port,
// Send message
send: (message, transfer = null) => {
if (transfer) {
port.postMessage(message, transfer);
} else {
port.postMessage(message);
}
},
// Listen for messages
onMessage: (callback) => {
port.addEventListener('message', (event) => {
callback(event.data, event);
});
},
// Handle errors
onError: (callback) => {
sharedWorker.addEventListener('error', callback);
port.addEventListener('messageerror', callback);
},
// Close connection
close: () => {
port.close();
this.activeWorkers.delete(workerId);
Logger.info(`[WorkerManager] Shared Worker closed: ${workerId}`);
}
};
this.activeWorkers.set(workerId, workerWrapper);
Logger.info(`[WorkerManager] Shared Worker created: ${workerId}`);
return workerWrapper;
} catch (error) {
Logger.error('[WorkerManager] Shared Worker creation failed:', error);
throw error;
}
}
};
/**
* Offscreen Canvas for worker-based rendering
*/
offscreen = {
// Create offscreen canvas worker
create: (canvas, workerScript = null) => {
if (!this.support.offscreenCanvas) {
throw new Error('Offscreen Canvas not supported');
}
try {
const offscreenCanvas = canvas.transferControlToOffscreen();
const defaultWorkerScript = `
self.addEventListener('message', function(e) {
const { canvas, type, data } = e.data;
if (type === 'init') {
self.canvas = canvas;
self.ctx = canvas.getContext('2d');
}
if (type === 'draw' && self.ctx) {
// Basic drawing operations
const { operations } = data;
operations.forEach(op => {
switch (op.type) {
case 'fillRect':
self.ctx.fillRect(...op.args);
break;
case 'strokeRect':
self.ctx.strokeRect(...op.args);
break;
case 'fillText':
self.ctx.fillText(...op.args);
break;
case 'setFillStyle':
self.ctx.fillStyle = op.value;
break;
case 'setStrokeStyle':
self.ctx.strokeStyle = op.value;
break;
}
});
}
});
`;
const script = workerScript || defaultWorkerScript;
const worker = this.web.create(script);
// Initialize worker with canvas
worker.send({
type: 'init',
canvas: offscreenCanvas
}, [offscreenCanvas]);
return {
worker,
// Draw operations
draw: (operations) => {
worker.send({
type: 'draw',
data: { operations }
});
},
// Send custom message
send: (message) => worker.send(message),
// Listen for messages
onMessage: (callback) => worker.onMessage(callback),
// Terminate worker
terminate: () => worker.terminate()
};
} catch (error) {
Logger.error('[WorkerManager] Offscreen Canvas creation failed:', error);
throw error;
}
}
};
// Helper methods
createWorkerBlob(fn) {
const code = `
(function() {
const workerFunction = ${fn.toString()};
if (typeof workerFunction === 'function') {
// If function expects to be called immediately
if (workerFunction.length === 0) {
workerFunction();
}
}
// Standard worker message handling
self.addEventListener('message', function(e) {
if (e.data.type === 'execute' && e.data.function) {
try {
const fn = new Function('return ' + e.data.function)();
const result = fn(e.data.data);
self.postMessage({
id: e.data.id,
result
});
} catch (error) {
self.postMessage({
id: e.data.id,
error: error.message
});
}
}
});
})();
`;
return new Blob([code], { type: 'application/javascript' });
}
generateId(prefix = 'worker') {
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Terminate all active workers
*/
terminateAll() {
this.activeWorkers.forEach(worker => {
if (worker.terminate) {
worker.terminate();
} else if (worker.close) {
worker.close();
}
});
this.activeWorkers.clear();
Logger.info('[WorkerManager] All workers terminated');
}
/**
* Get worker statistics
*/
getStats() {
const workers = Array.from(this.activeWorkers.values());
return {
total: workers.length,
byType: workers.reduce((acc, worker) => {
const type = worker.id.split('_')[0];
acc[type] = (acc[type] || 0) + 1;
return acc;
}, {}),
support: this.support,
hardwareConcurrency: navigator.hardwareConcurrency || 'unknown'
};
}
/**
* Create common worker utilities
*/
utils = {
// Benchmark function performance
benchmark: async (fn, data, iterations = 1000) => {
const worker = this.web.create(() => {
self.addEventListener('message', (e) => {
const { id, function: fnString, data, iterations } = e.data;
try {
const fn = new Function('return ' + fnString)();
const start = performance.now();
for (let i = 0; i < iterations; i++) {
fn(data);
}
const end = performance.now();
const totalTime = end - start;
const avgTime = totalTime / iterations;
self.postMessage({
id,
result: {
totalTime,
avgTime,
iterations,
opsPerSecond: 1000 / avgTime
}
});
} catch (error) {
self.postMessage({ id, error: error.message });
}
});
});
const result = await worker.execute(fn, data, iterations);
worker.terminate();
return result;
},
// Parallel array processing
parallelMap: async (array, fn, chunkSize = null) => {
const chunks = chunkSize || Math.ceil(array.length / (navigator.hardwareConcurrency || 4));
const pool = this.web.createPool(() => {
self.addEventListener('message', (e) => {
const { id, data: { chunk, function: fnString } } = e.data;
try {
const fn = new Function('return ' + fnString)();
const result = chunk.map(fn);
self.postMessage({ id, result });
} catch (error) {
self.postMessage({ id, error: error.message });
}
});
});
const promises = [];
for (let i = 0; i < array.length; i += chunks) {
const chunk = array.slice(i, i + chunks);
promises.push(pool.execute(fn, chunk));
}
const results = await Promise.all(promises);
pool.terminate();
return results.flat();
}
};
}