@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.
@@ -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;