@ruvector/edge-net 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/credits.js +631 -0
- package/package.json +9 -1
- package/task-execution-handler.js +868 -0
- package/tests/webrtc-datachannel-e2e-test.js +1081 -0
- package/webrtc.js +15 -16
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ruvector/edge-net Task Execution Handler
|
|
3
|
+
*
|
|
4
|
+
* Wires Firebase signaling to the worker pool for distributed task execution.
|
|
5
|
+
* When a node receives a 'task-assign' signal, it validates and executes the task,
|
|
6
|
+
* then sends the result back to the originator.
|
|
7
|
+
*
|
|
8
|
+
* Signal types:
|
|
9
|
+
* - 'task-assign' - Assign a task to a peer for execution
|
|
10
|
+
* - 'task-result' - Return successful result to originator
|
|
11
|
+
* - 'task-error' - Report execution failure to originator
|
|
12
|
+
* - 'task-progress' - Optional progress updates during execution
|
|
13
|
+
*
|
|
14
|
+
* Credit Integration:
|
|
15
|
+
* - Workers automatically earn credits when completing tasks
|
|
16
|
+
* - Task submitters spend credits when submitting tasks
|
|
17
|
+
* - Credits tracked via CRDT ledger for conflict-free replication
|
|
18
|
+
*
|
|
19
|
+
* @module @ruvector/edge-net/task-execution-handler
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { EventEmitter } from 'events';
|
|
23
|
+
import { randomBytes, createHash } from 'crypto';
|
|
24
|
+
|
|
25
|
+
// ============================================
|
|
26
|
+
// TASK VALIDATION
|
|
27
|
+
// ============================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validates incoming task assignments
|
|
31
|
+
*/
|
|
32
|
+
export class TaskValidator {
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
this.maxDataSize = options.maxDataSize || 1024 * 1024; // 1MB default
|
|
35
|
+
this.allowedTypes = options.allowedTypes || [
|
|
36
|
+
'embed', 'process', 'analyze', 'transform', 'compute', 'aggregate', 'custom'
|
|
37
|
+
];
|
|
38
|
+
this.maxPriority = options.maxPriority || 10;
|
|
39
|
+
this.requireSignature = options.requireSignature !== false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate a task assignment
|
|
44
|
+
* @param {Object} task - The task to validate
|
|
45
|
+
* @param {Object} signal - The signal containing the task
|
|
46
|
+
* @returns {Object} Validation result { valid: boolean, errors: string[] }
|
|
47
|
+
*/
|
|
48
|
+
validate(task, signal = {}) {
|
|
49
|
+
const errors = [];
|
|
50
|
+
|
|
51
|
+
// Required fields
|
|
52
|
+
if (!task) {
|
|
53
|
+
return { valid: false, errors: ['Task is required'] };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!task.id) {
|
|
57
|
+
errors.push('Task ID is required');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!task.type) {
|
|
61
|
+
errors.push('Task type is required');
|
|
62
|
+
} else if (!this.allowedTypes.includes(task.type)) {
|
|
63
|
+
errors.push(`Invalid task type: ${task.type}. Allowed: ${this.allowedTypes.join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (task.data === undefined) {
|
|
67
|
+
errors.push('Task data is required');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Data size check
|
|
71
|
+
const dataSize = this._estimateSize(task.data);
|
|
72
|
+
if (dataSize > this.maxDataSize) {
|
|
73
|
+
errors.push(`Task data exceeds max size (${dataSize} > ${this.maxDataSize})`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Priority check
|
|
77
|
+
if (task.priority !== undefined) {
|
|
78
|
+
const priority = typeof task.priority === 'string'
|
|
79
|
+
? this._priorityToNumber(task.priority)
|
|
80
|
+
: task.priority;
|
|
81
|
+
if (priority < 0 || priority > this.maxPriority) {
|
|
82
|
+
errors.push(`Invalid priority: ${task.priority}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Capability check (if specified)
|
|
87
|
+
if (task.requiredCapabilities && !Array.isArray(task.requiredCapabilities)) {
|
|
88
|
+
errors.push('requiredCapabilities must be an array');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Signature validation (if required and available)
|
|
92
|
+
if (this.requireSignature && signal.signature) {
|
|
93
|
+
if (!this._verifySignature(task, signal)) {
|
|
94
|
+
errors.push('Invalid task signature');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Timeout check
|
|
99
|
+
if (task.timeout !== undefined) {
|
|
100
|
+
if (typeof task.timeout !== 'number' || task.timeout <= 0) {
|
|
101
|
+
errors.push('Timeout must be a positive number');
|
|
102
|
+
}
|
|
103
|
+
if (task.timeout > 600000) { // 10 minutes max
|
|
104
|
+
errors.push('Timeout exceeds maximum (600000ms)');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
valid: errors.length === 0,
|
|
110
|
+
errors
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if local node has required capabilities
|
|
116
|
+
*/
|
|
117
|
+
hasCapabilities(required, available) {
|
|
118
|
+
if (!required || required.length === 0) return true;
|
|
119
|
+
if (!available || available.length === 0) return false;
|
|
120
|
+
return required.every(cap => available.includes(cap));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_estimateSize(data) {
|
|
124
|
+
if (data === null || data === undefined) return 0;
|
|
125
|
+
if (typeof data === 'string') return data.length;
|
|
126
|
+
try {
|
|
127
|
+
return JSON.stringify(data).length;
|
|
128
|
+
} catch {
|
|
129
|
+
return this.maxDataSize + 1; // Fail validation if can't serialize
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_priorityToNumber(priority) {
|
|
134
|
+
const map = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
135
|
+
return map[priority.toLowerCase()] ?? 2;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_verifySignature(task, signal) {
|
|
139
|
+
// Basic signature verification - in production use WASM crypto
|
|
140
|
+
if (!signal.signature || !signal.publicKey) return false;
|
|
141
|
+
// Simplified: just check signature exists and has correct format
|
|
142
|
+
return typeof signal.signature === 'string' && signal.signature.length >= 64;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ============================================
|
|
147
|
+
// TASK EXECUTION HANDLER
|
|
148
|
+
// ============================================
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* TaskExecutionHandler - Bridges signaling and worker pool for distributed execution
|
|
152
|
+
*
|
|
153
|
+
* Listens for 'task-assign' signals from FirebaseSignaling,
|
|
154
|
+
* validates and executes tasks using RealWorkerPool,
|
|
155
|
+
* and sends results back via signaling.
|
|
156
|
+
*/
|
|
157
|
+
export class TaskExecutionHandler extends EventEmitter {
|
|
158
|
+
/**
|
|
159
|
+
* @param {Object} options
|
|
160
|
+
* @param {FirebaseSignaling} options.signaling - Firebase signaling instance
|
|
161
|
+
* @param {RealWorkerPool} options.workerPool - Worker pool for execution
|
|
162
|
+
* @param {string[]} options.capabilities - This node's capabilities
|
|
163
|
+
* @param {Object} options.secureAccess - WASM secure access manager (optional)
|
|
164
|
+
*/
|
|
165
|
+
constructor(options = {}) {
|
|
166
|
+
super();
|
|
167
|
+
|
|
168
|
+
this.signaling = options.signaling;
|
|
169
|
+
this.workerPool = options.workerPool;
|
|
170
|
+
this.capabilities = options.capabilities || ['compute', 'process', 'embed'];
|
|
171
|
+
this.secureAccess = options.secureAccess || null;
|
|
172
|
+
this.nodeId = options.nodeId || options.signaling?.peerId;
|
|
173
|
+
|
|
174
|
+
// Task tracking
|
|
175
|
+
this.activeTasks = new Map(); // taskId -> { task, startTime, from }
|
|
176
|
+
this.completedTasks = new Map(); // taskId -> { result, duration }
|
|
177
|
+
this.taskTimeouts = new Map(); // taskId -> timeout handle
|
|
178
|
+
|
|
179
|
+
// Configuration
|
|
180
|
+
this.defaultTimeout = options.defaultTimeout || 60000;
|
|
181
|
+
this.maxConcurrentTasks = options.maxConcurrentTasks || 10;
|
|
182
|
+
this.reportProgress = options.reportProgress !== false;
|
|
183
|
+
this.progressInterval = options.progressInterval || 5000;
|
|
184
|
+
|
|
185
|
+
// Validator
|
|
186
|
+
this.validator = new TaskValidator({
|
|
187
|
+
requireSignature: options.requireSignature !== false,
|
|
188
|
+
allowedTypes: options.allowedTypes,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Stats
|
|
192
|
+
this.stats = {
|
|
193
|
+
tasksReceived: 0,
|
|
194
|
+
tasksExecuted: 0,
|
|
195
|
+
tasksFailed: 0,
|
|
196
|
+
tasksRejected: 0,
|
|
197
|
+
totalExecutionTime: 0,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Bind event handlers
|
|
201
|
+
this._boundHandlers = {
|
|
202
|
+
onSignal: this._handleSignal.bind(this),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
this.attached = false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Attach to signaling - start listening for task assignments
|
|
210
|
+
*/
|
|
211
|
+
attach() {
|
|
212
|
+
if (this.attached) return this;
|
|
213
|
+
if (!this.signaling) {
|
|
214
|
+
throw new Error('Signaling instance required');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Listen for all signals and filter for task-related ones
|
|
218
|
+
this.signaling.on('signal', this._boundHandlers.onSignal);
|
|
219
|
+
|
|
220
|
+
this.attached = true;
|
|
221
|
+
this.emit('attached');
|
|
222
|
+
|
|
223
|
+
console.log(`[TaskExecutionHandler] Attached to signaling, capabilities: ${this.capabilities.join(', ')}`);
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Detach from signaling - stop listening
|
|
229
|
+
*/
|
|
230
|
+
detach() {
|
|
231
|
+
if (!this.attached) return this;
|
|
232
|
+
|
|
233
|
+
this.signaling.off('signal', this._boundHandlers.onSignal);
|
|
234
|
+
|
|
235
|
+
// Clear all timeouts
|
|
236
|
+
for (const timeout of this.taskTimeouts.values()) {
|
|
237
|
+
clearTimeout(timeout);
|
|
238
|
+
}
|
|
239
|
+
this.taskTimeouts.clear();
|
|
240
|
+
|
|
241
|
+
this.attached = false;
|
|
242
|
+
this.emit('detached');
|
|
243
|
+
|
|
244
|
+
return this;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Handle incoming signal
|
|
249
|
+
*/
|
|
250
|
+
async _handleSignal(signal) {
|
|
251
|
+
const { type, from, data, verified } = signal;
|
|
252
|
+
|
|
253
|
+
switch (type) {
|
|
254
|
+
case 'task-assign':
|
|
255
|
+
await this._handleTaskAssign(from, data, signal);
|
|
256
|
+
break;
|
|
257
|
+
|
|
258
|
+
case 'task-result':
|
|
259
|
+
this._handleTaskResult(from, data);
|
|
260
|
+
break;
|
|
261
|
+
|
|
262
|
+
case 'task-error':
|
|
263
|
+
this._handleTaskError(from, data);
|
|
264
|
+
break;
|
|
265
|
+
|
|
266
|
+
case 'task-progress':
|
|
267
|
+
this._handleTaskProgress(from, data);
|
|
268
|
+
break;
|
|
269
|
+
|
|
270
|
+
case 'task-cancel':
|
|
271
|
+
await this._handleTaskCancel(from, data);
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Handle task assignment - validate, execute, return result
|
|
278
|
+
*/
|
|
279
|
+
async _handleTaskAssign(from, taskData, signal) {
|
|
280
|
+
this.stats.tasksReceived++;
|
|
281
|
+
|
|
282
|
+
// Handle various data formats
|
|
283
|
+
const task = taskData?.task || taskData || {};
|
|
284
|
+
const taskId = task.id || `recv-${randomBytes(8).toString('hex')}`;
|
|
285
|
+
|
|
286
|
+
console.log(`[TaskExecutionHandler] Received task ${taskId} from ${from?.slice(0, 8)}...`);
|
|
287
|
+
|
|
288
|
+
// Check capacity
|
|
289
|
+
if (this.activeTasks.size >= this.maxConcurrentTasks) {
|
|
290
|
+
this.stats.tasksRejected++;
|
|
291
|
+
await this._sendError(from, taskId, 'Node at capacity', 'CAPACITY_EXCEEDED');
|
|
292
|
+
this.emit('task-rejected', { taskId, from, reason: 'capacity' });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Validate task
|
|
297
|
+
const validation = this.validator.validate(task, signal);
|
|
298
|
+
if (!validation.valid) {
|
|
299
|
+
this.stats.tasksRejected++;
|
|
300
|
+
await this._sendError(from, taskId, validation.errors.join('; '), 'VALIDATION_FAILED');
|
|
301
|
+
this.emit('task-rejected', { taskId, from, reason: 'validation', errors: validation.errors });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check capabilities
|
|
306
|
+
if (!this.validator.hasCapabilities(task.requiredCapabilities, this.capabilities)) {
|
|
307
|
+
this.stats.tasksRejected++;
|
|
308
|
+
await this._sendError(from, taskId, 'Missing required capabilities', 'CAPABILITY_MISMATCH');
|
|
309
|
+
this.emit('task-rejected', { taskId, from, reason: 'capabilities' });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Track task
|
|
314
|
+
const taskInfo = {
|
|
315
|
+
task,
|
|
316
|
+
from,
|
|
317
|
+
startTime: Date.now(),
|
|
318
|
+
verified: signal.verified || false,
|
|
319
|
+
};
|
|
320
|
+
this.activeTasks.set(taskId, taskInfo);
|
|
321
|
+
|
|
322
|
+
// Set timeout
|
|
323
|
+
const timeout = task.timeout || this.defaultTimeout;
|
|
324
|
+
const timeoutHandle = setTimeout(() => {
|
|
325
|
+
this._handleTaskTimeout(taskId);
|
|
326
|
+
}, timeout);
|
|
327
|
+
this.taskTimeouts.set(taskId, timeoutHandle);
|
|
328
|
+
|
|
329
|
+
// Start progress reporting if enabled
|
|
330
|
+
let progressHandle = null;
|
|
331
|
+
if (this.reportProgress && this.progressInterval > 0) {
|
|
332
|
+
progressHandle = setInterval(() => {
|
|
333
|
+
this._sendProgress(from, taskId, {
|
|
334
|
+
status: 'running',
|
|
335
|
+
elapsed: Date.now() - taskInfo.startTime,
|
|
336
|
+
});
|
|
337
|
+
}, this.progressInterval);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Execute task
|
|
341
|
+
try {
|
|
342
|
+
this.emit('task-start', { taskId, from, type: task.type });
|
|
343
|
+
|
|
344
|
+
// Check if worker pool is ready
|
|
345
|
+
if (!this.workerPool || this.workerPool.status !== 'ready') {
|
|
346
|
+
throw new Error('Worker pool not ready');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Execute via worker pool
|
|
350
|
+
const result = await this.workerPool.execute(
|
|
351
|
+
task.type,
|
|
352
|
+
task.data,
|
|
353
|
+
task.options || {}
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// Task completed successfully
|
|
357
|
+
const duration = Date.now() - taskInfo.startTime;
|
|
358
|
+
this.stats.tasksExecuted++;
|
|
359
|
+
this.stats.totalExecutionTime += duration;
|
|
360
|
+
|
|
361
|
+
// Clear timeout and progress
|
|
362
|
+
clearTimeout(timeoutHandle);
|
|
363
|
+
this.taskTimeouts.delete(taskId);
|
|
364
|
+
if (progressHandle) clearInterval(progressHandle);
|
|
365
|
+
|
|
366
|
+
// Store completed task
|
|
367
|
+
this.completedTasks.set(taskId, { result, duration });
|
|
368
|
+
this.activeTasks.delete(taskId);
|
|
369
|
+
|
|
370
|
+
// Send result back
|
|
371
|
+
await this._sendResult(from, taskId, result, duration);
|
|
372
|
+
|
|
373
|
+
this.emit('task-complete', { taskId, from, duration, result });
|
|
374
|
+
|
|
375
|
+
console.log(`[TaskExecutionHandler] Task ${taskId} completed in ${duration}ms`);
|
|
376
|
+
|
|
377
|
+
} catch (error) {
|
|
378
|
+
// Task failed
|
|
379
|
+
const duration = Date.now() - taskInfo.startTime;
|
|
380
|
+
this.stats.tasksFailed++;
|
|
381
|
+
|
|
382
|
+
// Clear timeout and progress
|
|
383
|
+
clearTimeout(timeoutHandle);
|
|
384
|
+
this.taskTimeouts.delete(taskId);
|
|
385
|
+
if (progressHandle) clearInterval(progressHandle);
|
|
386
|
+
|
|
387
|
+
this.activeTasks.delete(taskId);
|
|
388
|
+
|
|
389
|
+
// Send error back
|
|
390
|
+
await this._sendError(from, taskId, error.message, 'EXECUTION_FAILED');
|
|
391
|
+
|
|
392
|
+
this.emit('task-error', { taskId, from, error: error.message, duration });
|
|
393
|
+
|
|
394
|
+
console.error(`[TaskExecutionHandler] Task ${taskId} failed:`, error.message);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Handle task timeout
|
|
400
|
+
*/
|
|
401
|
+
async _handleTaskTimeout(taskId) {
|
|
402
|
+
const taskInfo = this.activeTasks.get(taskId);
|
|
403
|
+
if (!taskInfo) return;
|
|
404
|
+
|
|
405
|
+
this.stats.tasksFailed++;
|
|
406
|
+
this.activeTasks.delete(taskId);
|
|
407
|
+
this.taskTimeouts.delete(taskId);
|
|
408
|
+
|
|
409
|
+
await this._sendError(taskInfo.from, taskId, 'Task execution timed out', 'TIMEOUT');
|
|
410
|
+
|
|
411
|
+
this.emit('task-timeout', { taskId, from: taskInfo.from });
|
|
412
|
+
|
|
413
|
+
console.warn(`[TaskExecutionHandler] Task ${taskId} timed out`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Handle incoming task result (when we submitted a task)
|
|
418
|
+
*/
|
|
419
|
+
_handleTaskResult(from, data) {
|
|
420
|
+
const { taskId, result, duration, processedBy } = data;
|
|
421
|
+
|
|
422
|
+
this.emit('result-received', {
|
|
423
|
+
taskId,
|
|
424
|
+
from,
|
|
425
|
+
result,
|
|
426
|
+
duration,
|
|
427
|
+
processedBy,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Handle incoming task error (when we submitted a task)
|
|
433
|
+
*/
|
|
434
|
+
_handleTaskError(from, data) {
|
|
435
|
+
const { taskId, error, code } = data;
|
|
436
|
+
|
|
437
|
+
this.emit('error-received', {
|
|
438
|
+
taskId,
|
|
439
|
+
from,
|
|
440
|
+
error,
|
|
441
|
+
code,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Handle incoming progress update
|
|
447
|
+
*/
|
|
448
|
+
_handleTaskProgress(from, data) {
|
|
449
|
+
const { taskId, progress, status, elapsed } = data;
|
|
450
|
+
|
|
451
|
+
this.emit('progress-received', {
|
|
452
|
+
taskId,
|
|
453
|
+
from,
|
|
454
|
+
progress,
|
|
455
|
+
status,
|
|
456
|
+
elapsed,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Handle task cancellation request
|
|
462
|
+
*/
|
|
463
|
+
async _handleTaskCancel(from, data) {
|
|
464
|
+
const { taskId } = data;
|
|
465
|
+
const taskInfo = this.activeTasks.get(taskId);
|
|
466
|
+
|
|
467
|
+
if (!taskInfo) {
|
|
468
|
+
return; // Task not found or already completed
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Verify cancellation is from original submitter
|
|
472
|
+
if (taskInfo.from !== from) {
|
|
473
|
+
console.warn(`[TaskExecutionHandler] Unauthorized cancel request for ${taskId}`);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Clear timeout
|
|
478
|
+
const timeout = this.taskTimeouts.get(taskId);
|
|
479
|
+
if (timeout) {
|
|
480
|
+
clearTimeout(timeout);
|
|
481
|
+
this.taskTimeouts.delete(taskId);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
this.activeTasks.delete(taskId);
|
|
485
|
+
this.emit('task-cancelled', { taskId, from });
|
|
486
|
+
|
|
487
|
+
console.log(`[TaskExecutionHandler] Task ${taskId} cancelled by originator`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Send task result back to originator
|
|
492
|
+
*/
|
|
493
|
+
async _sendResult(to, taskId, result, duration) {
|
|
494
|
+
if (!this.signaling?.isConnected) return;
|
|
495
|
+
|
|
496
|
+
await this.signaling.sendSignal(to, 'task-result', {
|
|
497
|
+
taskId,
|
|
498
|
+
result,
|
|
499
|
+
duration,
|
|
500
|
+
processedBy: this.nodeId,
|
|
501
|
+
success: true,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Send error back to originator
|
|
507
|
+
*/
|
|
508
|
+
async _sendError(to, taskId, error, code = 'ERROR') {
|
|
509
|
+
if (!this.signaling?.isConnected) return;
|
|
510
|
+
|
|
511
|
+
await this.signaling.sendSignal(to, 'task-error', {
|
|
512
|
+
taskId,
|
|
513
|
+
error,
|
|
514
|
+
code,
|
|
515
|
+
processedBy: this.nodeId,
|
|
516
|
+
success: false,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Send progress update to originator
|
|
522
|
+
*/
|
|
523
|
+
async _sendProgress(to, taskId, progressData) {
|
|
524
|
+
if (!this.signaling?.isConnected) return;
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
await this.signaling.sendSignal(to, 'task-progress', {
|
|
528
|
+
taskId,
|
|
529
|
+
...progressData,
|
|
530
|
+
processedBy: this.nodeId,
|
|
531
|
+
});
|
|
532
|
+
} catch {
|
|
533
|
+
// Ignore progress send errors
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Submit a task to a remote peer
|
|
539
|
+
* @param {string} toPeerId - Target peer ID
|
|
540
|
+
* @param {Object} task - Task to submit
|
|
541
|
+
* @param {Object} options - Submission options
|
|
542
|
+
* @returns {Promise<Object>} Task result
|
|
543
|
+
*/
|
|
544
|
+
async submitTask(toPeerId, task, options = {}) {
|
|
545
|
+
if (!this.signaling?.isConnected) {
|
|
546
|
+
throw new Error('Signaling not connected');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const taskId = task.id || `submit-${randomBytes(8).toString('hex')}`;
|
|
550
|
+
const timeout = options.timeout || this.defaultTimeout;
|
|
551
|
+
|
|
552
|
+
// Create promise for result
|
|
553
|
+
return new Promise((resolve, reject) => {
|
|
554
|
+
// Set up result listener
|
|
555
|
+
const onResult = (data) => {
|
|
556
|
+
if (data.taskId === taskId) {
|
|
557
|
+
cleanup();
|
|
558
|
+
resolve(data);
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const onError = (data) => {
|
|
563
|
+
if (data.taskId === taskId) {
|
|
564
|
+
cleanup();
|
|
565
|
+
reject(new Error(data.error || 'Task failed'));
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const timeoutHandle = setTimeout(() => {
|
|
570
|
+
cleanup();
|
|
571
|
+
reject(new Error('Task submission timed out'));
|
|
572
|
+
}, timeout);
|
|
573
|
+
|
|
574
|
+
const cleanup = () => {
|
|
575
|
+
clearTimeout(timeoutHandle);
|
|
576
|
+
this.off('result-received', onResult);
|
|
577
|
+
this.off('error-received', onError);
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
this.on('result-received', onResult);
|
|
581
|
+
this.on('error-received', onError);
|
|
582
|
+
|
|
583
|
+
// Send task assignment
|
|
584
|
+
this.signaling.sendSignal(toPeerId, 'task-assign', {
|
|
585
|
+
task: {
|
|
586
|
+
id: taskId,
|
|
587
|
+
type: task.type,
|
|
588
|
+
data: task.data,
|
|
589
|
+
options: task.options,
|
|
590
|
+
priority: task.priority || 'medium',
|
|
591
|
+
requiredCapabilities: task.requiredCapabilities,
|
|
592
|
+
timeout,
|
|
593
|
+
},
|
|
594
|
+
}).catch(err => {
|
|
595
|
+
cleanup();
|
|
596
|
+
reject(err);
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Broadcast task to first available peer
|
|
603
|
+
* @param {Object} task - Task to execute
|
|
604
|
+
* @param {Object} options - Options
|
|
605
|
+
* @returns {Promise<Object>} Task result
|
|
606
|
+
*/
|
|
607
|
+
async broadcastTask(task, options = {}) {
|
|
608
|
+
if (!this.signaling?.isConnected) {
|
|
609
|
+
throw new Error('Signaling not connected');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const peers = this.signaling.getOnlinePeers();
|
|
613
|
+
if (peers.length === 0) {
|
|
614
|
+
throw new Error('No peers available');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Filter peers by capabilities if required
|
|
618
|
+
const requiredCaps = task.requiredCapabilities || [];
|
|
619
|
+
const eligiblePeers = requiredCaps.length > 0
|
|
620
|
+
? peers.filter(p => requiredCaps.every(c => p.capabilities?.includes(c)))
|
|
621
|
+
: peers;
|
|
622
|
+
|
|
623
|
+
if (eligiblePeers.length === 0) {
|
|
624
|
+
throw new Error('No peers with required capabilities');
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Try peers in order until one succeeds
|
|
628
|
+
const errors = [];
|
|
629
|
+
for (const peer of eligiblePeers) {
|
|
630
|
+
try {
|
|
631
|
+
return await this.submitTask(peer.id, task, options);
|
|
632
|
+
} catch (err) {
|
|
633
|
+
errors.push({ peer: peer.id, error: err.message });
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
throw new Error(`All peers failed: ${JSON.stringify(errors)}`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Get handler status
|
|
642
|
+
*/
|
|
643
|
+
getStatus() {
|
|
644
|
+
return {
|
|
645
|
+
attached: this.attached,
|
|
646
|
+
nodeId: this.nodeId,
|
|
647
|
+
capabilities: this.capabilities,
|
|
648
|
+
activeTasks: this.activeTasks.size,
|
|
649
|
+
completedTasks: this.completedTasks.size,
|
|
650
|
+
stats: { ...this.stats },
|
|
651
|
+
avgExecutionTime: this.stats.tasksExecuted > 0
|
|
652
|
+
? this.stats.totalExecutionTime / this.stats.tasksExecuted
|
|
653
|
+
: 0,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ============================================
|
|
659
|
+
// AUTO-WIRE INTEGRATION
|
|
660
|
+
// ============================================
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Create and wire task execution when a node joins the network
|
|
664
|
+
*
|
|
665
|
+
* @param {Object} options
|
|
666
|
+
* @param {FirebaseSignaling} options.signaling - Firebase signaling instance
|
|
667
|
+
* @param {RealWorkerPool} options.workerPool - Worker pool (will create if not provided)
|
|
668
|
+
* @param {Object} options.secureAccess - Secure access manager (optional)
|
|
669
|
+
* @returns {Promise<TaskExecutionHandler>} Wired handler
|
|
670
|
+
*/
|
|
671
|
+
export async function createTaskExecutionWiring(options = {}) {
|
|
672
|
+
const { signaling, secureAccess } = options;
|
|
673
|
+
|
|
674
|
+
if (!signaling) {
|
|
675
|
+
throw new Error('Signaling instance required for task execution wiring');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Create or use provided worker pool
|
|
679
|
+
let workerPool = options.workerPool;
|
|
680
|
+
if (!workerPool) {
|
|
681
|
+
const { RealWorkerPool } = await import('./real-workers.js');
|
|
682
|
+
workerPool = new RealWorkerPool({ size: 2 });
|
|
683
|
+
await workerPool.initialize();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Create handler
|
|
687
|
+
const handler = new TaskExecutionHandler({
|
|
688
|
+
signaling,
|
|
689
|
+
workerPool,
|
|
690
|
+
secureAccess,
|
|
691
|
+
nodeId: signaling.peerId,
|
|
692
|
+
capabilities: options.capabilities || ['compute', 'process', 'embed', 'transform', 'analyze'],
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// Attach to signaling
|
|
696
|
+
handler.attach();
|
|
697
|
+
|
|
698
|
+
// Log task events
|
|
699
|
+
handler.on('task-start', ({ taskId, from, type }) => {
|
|
700
|
+
console.log(` [Task] Starting ${type} task ${taskId.slice(0, 8)}... from ${from?.slice(0, 8)}...`);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
handler.on('task-complete', ({ taskId, duration }) => {
|
|
704
|
+
console.log(` [Task] Completed ${taskId.slice(0, 8)}... in ${duration}ms`);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
handler.on('task-error', ({ taskId, error }) => {
|
|
708
|
+
console.log(` [Task] Failed ${taskId.slice(0, 8)}...: ${error}`);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
return handler;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Integration class that auto-wires everything when a node joins
|
|
716
|
+
*/
|
|
717
|
+
export class DistributedTaskNetwork extends EventEmitter {
|
|
718
|
+
constructor(options = {}) {
|
|
719
|
+
super();
|
|
720
|
+
|
|
721
|
+
this.signaling = null;
|
|
722
|
+
this.workerPool = null;
|
|
723
|
+
this.handler = null;
|
|
724
|
+
this.secureAccess = options.secureAccess || null;
|
|
725
|
+
|
|
726
|
+
this.config = {
|
|
727
|
+
room: options.room || 'default',
|
|
728
|
+
peerId: options.peerId,
|
|
729
|
+
capabilities: options.capabilities || ['compute', 'process', 'embed'],
|
|
730
|
+
firebaseConfig: options.firebaseConfig,
|
|
731
|
+
autoInitWorkers: options.autoInitWorkers !== false,
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
this.connected = false;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Initialize and join the distributed task network
|
|
739
|
+
*/
|
|
740
|
+
async join() {
|
|
741
|
+
console.log('\n[DistributedTaskNetwork] Joining network...');
|
|
742
|
+
|
|
743
|
+
// Initialize Firebase signaling
|
|
744
|
+
const { FirebaseSignaling } = await import('./firebase-signaling.js');
|
|
745
|
+
this.signaling = new FirebaseSignaling({
|
|
746
|
+
peerId: this.config.peerId,
|
|
747
|
+
room: this.config.room,
|
|
748
|
+
firebaseConfig: this.config.firebaseConfig,
|
|
749
|
+
secureAccess: this.secureAccess,
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
// Connect to Firebase
|
|
753
|
+
const connected = await this.signaling.connect();
|
|
754
|
+
if (!connected) {
|
|
755
|
+
throw new Error('Failed to connect to Firebase signaling');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Initialize worker pool
|
|
759
|
+
if (this.config.autoInitWorkers) {
|
|
760
|
+
const { RealWorkerPool } = await import('./real-workers.js');
|
|
761
|
+
this.workerPool = new RealWorkerPool({ size: 2 });
|
|
762
|
+
await this.workerPool.initialize();
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Create and attach task handler
|
|
766
|
+
this.handler = new TaskExecutionHandler({
|
|
767
|
+
signaling: this.signaling,
|
|
768
|
+
workerPool: this.workerPool,
|
|
769
|
+
secureAccess: this.secureAccess,
|
|
770
|
+
nodeId: this.signaling.peerId,
|
|
771
|
+
capabilities: this.config.capabilities,
|
|
772
|
+
});
|
|
773
|
+
this.handler.attach();
|
|
774
|
+
|
|
775
|
+
// Forward events
|
|
776
|
+
this.handler.on('task-complete', (data) => this.emit('task-complete', data));
|
|
777
|
+
this.handler.on('task-error', (data) => this.emit('task-error', data));
|
|
778
|
+
this.handler.on('result-received', (data) => this.emit('result-received', data));
|
|
779
|
+
|
|
780
|
+
this.connected = true;
|
|
781
|
+
|
|
782
|
+
console.log(`[DistributedTaskNetwork] Joined as ${this.signaling.peerId.slice(0, 8)}...`);
|
|
783
|
+
console.log(`[DistributedTaskNetwork] Capabilities: ${this.config.capabilities.join(', ')}`);
|
|
784
|
+
|
|
785
|
+
this.emit('joined', {
|
|
786
|
+
peerId: this.signaling.peerId,
|
|
787
|
+
capabilities: this.config.capabilities,
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
return this;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Submit a task for distributed execution
|
|
795
|
+
*/
|
|
796
|
+
async submitTask(task, options = {}) {
|
|
797
|
+
if (!this.connected || !this.handler) {
|
|
798
|
+
throw new Error('Not connected to network');
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// If specific peer, submit directly
|
|
802
|
+
if (options.targetPeer) {
|
|
803
|
+
return this.handler.submitTask(options.targetPeer, task, options);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Otherwise broadcast to find available peer
|
|
807
|
+
return this.handler.broadcastTask(task, options);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Execute task locally (bypass network)
|
|
812
|
+
*/
|
|
813
|
+
async executeLocally(task) {
|
|
814
|
+
if (!this.workerPool || this.workerPool.status !== 'ready') {
|
|
815
|
+
throw new Error('Worker pool not ready');
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return this.workerPool.execute(task.type, task.data, task.options);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Get online peers
|
|
823
|
+
*/
|
|
824
|
+
getPeers() {
|
|
825
|
+
return this.signaling?.getOnlinePeers() || [];
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Get network status
|
|
830
|
+
*/
|
|
831
|
+
getStatus() {
|
|
832
|
+
return {
|
|
833
|
+
connected: this.connected,
|
|
834
|
+
peerId: this.signaling?.peerId,
|
|
835
|
+
peers: this.signaling?.peers?.size || 0,
|
|
836
|
+
handler: this.handler?.getStatus(),
|
|
837
|
+
workerPool: this.workerPool?.getStatus(),
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Leave the network
|
|
843
|
+
*/
|
|
844
|
+
async leave() {
|
|
845
|
+
if (this.handler) {
|
|
846
|
+
this.handler.detach();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (this.workerPool) {
|
|
850
|
+
await this.workerPool.shutdown();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (this.signaling) {
|
|
854
|
+
await this.signaling.disconnect();
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
this.connected = false;
|
|
858
|
+
this.emit('left');
|
|
859
|
+
|
|
860
|
+
console.log('[DistributedTaskNetwork] Left network');
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// ============================================
|
|
865
|
+
// EXPORTS
|
|
866
|
+
// ============================================
|
|
867
|
+
|
|
868
|
+
export default TaskExecutionHandler;
|