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
JavaScript
"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;