- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
648 lines
25 KiB
JavaScript
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();
|
|
}
|
|
};
|
|
} |