UNPKG

woolball-client

Version:

Client-side library for Woolball enabling secure browser resource sharing for distributed AI task processing

324 lines (323 loc) 12.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../utils"); const web_worker_1 = __importDefault(require("web-worker")); const TaskAvailability_1 = require("./TaskAvailability"); class Woolball { constructor(id, url = 'ws://localhost:9003/ws', options = {}) { this.wsConnection = null; this.eventListeners = new Map(); this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectTimeout = 1000; this.reconnectTimer = null; this.destroyed = false; this.options = options; // Default to 'browser' environment if not specified if (!this.options.environment) { this.options.environment = 'browser'; } if (['browser', 'extension'].includes(this.options.environment)) { (0, utils_1.verifyBrowserCompatibility)(); } this.clientId = id; this.wsUrl = url; this.eventListeners.set('started', new Set()); this.eventListeners.set('success', new Set()); this.eventListeners.set('error', new Set()); this.eventListeners.set('node_count', new Set()); this.workerTypes = new Map(); this.activeWorkers = new Set(); // Initialize worker types based on task configurations Object.keys(TaskAvailability_1.TASK_CONFIGS).forEach((taskType) => { const handler = (0, TaskAvailability_1.getTaskHandler)(taskType, this.getCurrentEnvironment()); if (handler) { this.workerTypes.set(taskType, handler); } }); } start() { if (this.wsConnection) { console.warn('WebSocket connection already exists'); return; } this.destroyed = false; this.reconnectAttempts = 0; this.connectWebSocket(this.wsUrl); } destroy() { this.destroyed = true; if (this.reconnectTimer !== null) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.wsConnection) { this.wsConnection.close(); this.wsConnection = null; } this.activeWorkers.forEach(worker => { worker.terminate(); }); this.activeWorkers.clear(); this.eventListeners.clear(); this.eventListeners.set('started', new Set()); this.eventListeners.set('success', new Set()); this.eventListeners.set('error', new Set()); this.eventListeners.set('node_count', new Set()); this.workerTypes.clear(); } /** * Establishes WebSocket connection and sets up message handlers */ connectWebSocket(url) { this.wsConnection = new WebSocket(`${url}/${this.clientId}`); this.wsConnection.onopen = () => { console.log('WebSocket connection established'); this.reconnectAttempts = 0; }; this.wsConnection.onmessage = (event) => { if (event.data === 'ping') { return; } if (event.data.startsWith('node_count:')) { const nodeCountStr = event.data.split(':')[1]; const nodeCount = parseInt(nodeCountStr, 10); if (!isNaN(nodeCount)) { this.emitEvent('node_count', { id: '', type: 'node_count', status: 'node_count', nodeCount: nodeCount }); } return; } try { this.handleWebSocketMessage(JSON.parse(event.data)); } catch (parseError) { console.error('Failed to parse WebSocket message'); } }; this.wsConnection.onerror = (error) => { console.error('WebSocket error:', error); }; this.wsConnection.onclose = (event) => { console.log(`WebSocket connection closed: ${event.code} ${event.reason}`); this.wsConnection = null; if (!this.destroyed && event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; const delay = this.reconnectTimeout * Math.pow(2, this.reconnectAttempts - 1); console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null; this.connectWebSocket(url); }, delay); } }; } /** * Handles incoming WebSocket messages */ async handleWebSocketMessage(message) { const { Id, Key, Value } = message; if (!Id || !Key || !Value) { console.error('Invalid message format: missing Id, Key, or Value'); return; } try { this.emitEvent('started', { id: Id, type: Key, status: 'started' }); const response = await this.processEvent(Key, Value); if (response.error) { const errorData = { id: Id, type: Key, status: 'error', }; console.error(`Error processing ${Key}`); this.emitEvent('error', errorData); this.sendWebSocketMessage({ type: 'ERROR', data: { requestId: Id, error: response.error, } }); return; } this.emitEvent('success', { id: Id, type: Key, status: 'success', }); this.sendWebSocketMessage({ type: 'PROCESS_RESULT', data: { requestId: Id, response } }); } catch (error) { console.error('Error handling WebSocket message:', error); this.sendWebSocketMessage({ type: 'ERROR', data: { requestId: Id, error: error instanceof Error ? error.message : 'Unknown error', } }); } } /** * Sends a message to the WebSocket server */ sendWebSocketMessage(message) { if (this.wsConnection && this.wsConnection.readyState === WebSocket.OPEN) { this.wsConnection.send(JSON.stringify(message)); return true; } return false; } setWorkerSource(type, source) { this.workerTypes.set(type, source); } createWorker(type) { const workerSource = this.workerTypes.get(type); if (!workerSource) { throw new Error(`Worker type not found: ${type}`); } try { const blob = new Blob([workerSource], { type: 'application/javascript' }); const workerUrl = URL.createObjectURL(blob); const worker = new web_worker_1.default(workerUrl); URL.revokeObjectURL(workerUrl); this.activeWorkers.add(worker); return worker; } catch (error) { console.error('Error creating worker:', error); throw new Error(`Failed to create worker: ${error.message || 'Unknown error'}`); } } /** * Gets the current environment based on options or detection * @returns The current environment */ getCurrentEnvironment() { if (this.options.environment) { return this.options.environment; } else if (typeof window !== 'undefined') { return 'browser'; } else if (typeof process !== 'undefined' && process.versions && process.versions.node) { return 'node'; } else { // Fallback, though this should not happen in normal circumstances console.warn('Could not determine environment, defaulting to node'); return 'node'; } } terminateWorker(worker) { worker.terminate(); if (worker._blobUrl) { URL.revokeObjectURL(worker._blobUrl); delete worker._blobUrl; } this.activeWorkers.delete(worker); } async processEvent(type, value) { // Convert string boolean values to actual booleans (shallow copy to avoid mutating caller's object) const normalizedValue = { ...value }; for (const key in normalizedValue) { if (normalizedValue[key] === 'true') { normalizedValue[key] = true; } if (normalizedValue[key] === 'false') { normalizedValue[key] = false; } } const currentEnvironment = this.getCurrentEnvironment(); const executionType = (0, TaskAvailability_1.getTaskExecutionType)(type, currentEnvironment); if (!executionType) { return { error: `Task type '${type}' is not supported in ${currentEnvironment} environment` }; } // Handle tasks based on their execution type switch (executionType) { case 'browser': try { const handler = (0, TaskAvailability_1.getTaskHandler)(type, currentEnvironment); const result = await handler(normalizedValue); return result; } catch (processorError) { console.error(`[Browser] Error in ${type} processor:`, processorError); const errorMessage = processorError instanceof Error ? processorError.message : String(processorError); return { error: errorMessage }; } case 'node_worker': { // Validate provider type for Node.js (keep existing validation) if (normalizedValue.provider && normalizedValue.provider !== 'transformers') { throw new Error(`Unsupported provider for Node.js: ${normalizedValue.provider}. Only 'transformers' is supported.`); } const { processWithoutNodeWorker } = await import('./node-worker.js'); return processWithoutNodeWorker(type, normalizedValue); } case 'worker': const worker = this.createWorker(type); return new Promise((resolve, reject) => { const messageHandler = (e) => { worker.removeEventListener('message', messageHandler); worker.removeEventListener('error', errorHandler); this.terminateWorker(worker); if (e.data.error) { resolve({ error: e.data.error }); } else { resolve(e.data); } }; const errorHandler = (e) => { worker.removeEventListener('message', messageHandler); worker.removeEventListener('error', errorHandler); this.terminateWorker(worker); const errorMessage = e.message || 'Unknown worker error'; resolve({ error: errorMessage }); }; worker.addEventListener('message', messageHandler); worker.addEventListener('error', errorHandler); worker.postMessage(normalizedValue); }); default: return { error: `Unknown execution type: ${executionType}` }; } } on(status, listener) { const listeners = this.eventListeners.get(status); if (listeners) { listeners.add(listener); } } off(status, listener) { const listeners = this.eventListeners.get(status); if (listeners) { listeners.delete(listener); } } emitEvent(status, data) { const listeners = this.eventListeners.get(status); if (listeners) { listeners.forEach(listener => listener(data)); } } } exports.default = Woolball;