@ruvector/edge-net 0.4.2 → 0.4.4
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/deploy/.env.example +97 -0
- package/deploy/DEPLOY.md +481 -0
- package/deploy/Dockerfile +100 -0
- package/deploy/docker-compose.yml +162 -0
- package/deploy/genesis-prod.js +1550 -0
- package/deploy/health-check.js +187 -0
- package/deploy/prometheus.yml +38 -0
- package/firebase-signaling.js +57 -2
- package/package.json +8 -1
- package/real-workers.js +9 -4
- package/scheduler.js +8 -4
- package/tests/distributed-workers-test.js +1609 -0
- package/tests/multitenancy-test.js +130 -0
- package/tests/p2p-migration-test.js +1102 -0
- package/tests/task-execution-test.js +534 -0
- package/tests/webrtc-peer-test.js +686 -0
- package/webrtc.js +693 -40
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @ruvector/edge-net Genesis Node Health Check
|
|
4
|
+
*
|
|
5
|
+
* Standalone health check script for use with container orchestrators,
|
|
6
|
+
* load balancers, and monitoring systems.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node deploy/health-check.js # Check localhost:8788
|
|
10
|
+
* node deploy/health-check.js --host 10.0.0.1 # Check specific host
|
|
11
|
+
* node deploy/health-check.js --port 9000 # Check specific port
|
|
12
|
+
* node deploy/health-check.js --endpoint ready # Check readiness
|
|
13
|
+
* node deploy/health-check.js --json # JSON output
|
|
14
|
+
*
|
|
15
|
+
* Exit codes:
|
|
16
|
+
* 0 - Healthy
|
|
17
|
+
* 1 - Unhealthy or error
|
|
18
|
+
*
|
|
19
|
+
* @module @ruvector/edge-net/deploy/health-check
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import http from 'http';
|
|
23
|
+
|
|
24
|
+
// Parse arguments
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
const config = {
|
|
27
|
+
host: 'localhost',
|
|
28
|
+
port: 8788,
|
|
29
|
+
endpoint: 'health',
|
|
30
|
+
timeout: 5000,
|
|
31
|
+
json: false,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < args.length; i++) {
|
|
35
|
+
switch (args[i]) {
|
|
36
|
+
case '--host':
|
|
37
|
+
case '-h':
|
|
38
|
+
config.host = args[++i];
|
|
39
|
+
break;
|
|
40
|
+
case '--port':
|
|
41
|
+
case '-p':
|
|
42
|
+
config.port = parseInt(args[++i]);
|
|
43
|
+
break;
|
|
44
|
+
case '--endpoint':
|
|
45
|
+
case '-e':
|
|
46
|
+
config.endpoint = args[++i];
|
|
47
|
+
break;
|
|
48
|
+
case '--timeout':
|
|
49
|
+
case '-t':
|
|
50
|
+
config.timeout = parseInt(args[++i]);
|
|
51
|
+
break;
|
|
52
|
+
case '--json':
|
|
53
|
+
case '-j':
|
|
54
|
+
config.json = true;
|
|
55
|
+
break;
|
|
56
|
+
case '--help':
|
|
57
|
+
console.log(`
|
|
58
|
+
Genesis Node Health Check
|
|
59
|
+
|
|
60
|
+
Usage: node health-check.js [options]
|
|
61
|
+
|
|
62
|
+
Options:
|
|
63
|
+
--host, -h <host> Host to check (default: localhost)
|
|
64
|
+
--port, -p <port> Port to check (default: 8788)
|
|
65
|
+
--endpoint, -e <path> Endpoint to check: health, ready, status, metrics (default: health)
|
|
66
|
+
--timeout, -t <ms> Request timeout in milliseconds (default: 5000)
|
|
67
|
+
--json, -j Output JSON format
|
|
68
|
+
--help Show this help
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
node health-check.js
|
|
72
|
+
node health-check.js --host genesis.example.com --port 8788
|
|
73
|
+
node health-check.js --endpoint ready
|
|
74
|
+
node health-check.js --json
|
|
75
|
+
|
|
76
|
+
Exit Codes:
|
|
77
|
+
0 - Healthy/Ready
|
|
78
|
+
1 - Unhealthy/Not Ready/Error
|
|
79
|
+
`);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function checkHealth() {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const startTime = Date.now();
|
|
87
|
+
|
|
88
|
+
const req = http.get({
|
|
89
|
+
hostname: config.host,
|
|
90
|
+
port: config.port,
|
|
91
|
+
path: `/${config.endpoint}`,
|
|
92
|
+
timeout: config.timeout,
|
|
93
|
+
}, (res) => {
|
|
94
|
+
let data = '';
|
|
95
|
+
|
|
96
|
+
res.on('data', chunk => data += chunk);
|
|
97
|
+
res.on('end', () => {
|
|
98
|
+
const latency = Date.now() - startTime;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(data);
|
|
102
|
+
resolve({
|
|
103
|
+
healthy: res.statusCode === 200,
|
|
104
|
+
statusCode: res.statusCode,
|
|
105
|
+
latency,
|
|
106
|
+
data: parsed,
|
|
107
|
+
});
|
|
108
|
+
} catch {
|
|
109
|
+
resolve({
|
|
110
|
+
healthy: res.statusCode === 200,
|
|
111
|
+
statusCode: res.statusCode,
|
|
112
|
+
latency,
|
|
113
|
+
data: data,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
req.on('error', (err) => {
|
|
120
|
+
reject({
|
|
121
|
+
healthy: false,
|
|
122
|
+
error: err.message,
|
|
123
|
+
latency: Date.now() - startTime,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
req.on('timeout', () => {
|
|
128
|
+
req.destroy();
|
|
129
|
+
reject({
|
|
130
|
+
healthy: false,
|
|
131
|
+
error: 'Request timeout',
|
|
132
|
+
latency: config.timeout,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function main() {
|
|
139
|
+
try {
|
|
140
|
+
const result = await checkHealth();
|
|
141
|
+
|
|
142
|
+
if (config.json) {
|
|
143
|
+
console.log(JSON.stringify({
|
|
144
|
+
...result,
|
|
145
|
+
host: config.host,
|
|
146
|
+
port: config.port,
|
|
147
|
+
endpoint: config.endpoint,
|
|
148
|
+
timestamp: new Date().toISOString(),
|
|
149
|
+
}, null, 2));
|
|
150
|
+
} else {
|
|
151
|
+
if (result.healthy) {
|
|
152
|
+
console.log(`OK - ${config.host}:${config.port}/${config.endpoint} (${result.latency}ms)`);
|
|
153
|
+
|
|
154
|
+
if (result.data?.ready !== undefined) {
|
|
155
|
+
console.log(` Ready: ${result.data.ready}`);
|
|
156
|
+
}
|
|
157
|
+
if (result.data?.status) {
|
|
158
|
+
console.log(` Status: ${result.data.status}`);
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
console.log(`FAIL - ${config.host}:${config.port}/${config.endpoint}`);
|
|
162
|
+
console.log(` Status Code: ${result.statusCode}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
process.exit(result.healthy ? 0 : 1);
|
|
167
|
+
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (config.json) {
|
|
170
|
+
console.log(JSON.stringify({
|
|
171
|
+
healthy: false,
|
|
172
|
+
host: config.host,
|
|
173
|
+
port: config.port,
|
|
174
|
+
endpoint: config.endpoint,
|
|
175
|
+
error: error.error || error.message,
|
|
176
|
+
timestamp: new Date().toISOString(),
|
|
177
|
+
}, null, 2));
|
|
178
|
+
} else {
|
|
179
|
+
console.log(`ERROR - ${config.host}:${config.port}/${config.endpoint}`);
|
|
180
|
+
console.log(` Error: ${error.error || error.message}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
main();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Prometheus configuration for Edge-Net Genesis Node monitoring
|
|
2
|
+
#
|
|
3
|
+
# Scrapes metrics from genesis nodes at /metrics endpoint
|
|
4
|
+
|
|
5
|
+
global:
|
|
6
|
+
scrape_interval: 15s
|
|
7
|
+
evaluation_interval: 15s
|
|
8
|
+
external_labels:
|
|
9
|
+
cluster: 'edge-net-local'
|
|
10
|
+
|
|
11
|
+
alerting:
|
|
12
|
+
alertmanagers: []
|
|
13
|
+
|
|
14
|
+
rule_files: []
|
|
15
|
+
|
|
16
|
+
scrape_configs:
|
|
17
|
+
# Primary Genesis Node
|
|
18
|
+
- job_name: 'genesis'
|
|
19
|
+
static_configs:
|
|
20
|
+
- targets: ['genesis:8788']
|
|
21
|
+
labels:
|
|
22
|
+
instance: 'genesis-primary'
|
|
23
|
+
metrics_path: /metrics
|
|
24
|
+
scrape_interval: 10s
|
|
25
|
+
|
|
26
|
+
# Secondary Genesis Node (if cluster profile enabled)
|
|
27
|
+
- job_name: 'genesis-cluster'
|
|
28
|
+
static_configs:
|
|
29
|
+
- targets: ['genesis-2:8788']
|
|
30
|
+
labels:
|
|
31
|
+
instance: 'genesis-secondary'
|
|
32
|
+
metrics_path: /metrics
|
|
33
|
+
scrape_interval: 10s
|
|
34
|
+
|
|
35
|
+
# Prometheus self-monitoring
|
|
36
|
+
- job_name: 'prometheus'
|
|
37
|
+
static_configs:
|
|
38
|
+
- targets: ['localhost:9090']
|
package/firebase-signaling.js
CHANGED
|
@@ -423,6 +423,22 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
423
423
|
case 'ice-candidate':
|
|
424
424
|
this.emit('ice-candidate', { from: signal.from, candidate: signal.data, verified: !!signal.signature });
|
|
425
425
|
break;
|
|
426
|
+
// Task execution signal types
|
|
427
|
+
case 'task-assign':
|
|
428
|
+
case 'task-result':
|
|
429
|
+
case 'task-error':
|
|
430
|
+
case 'task-progress':
|
|
431
|
+
case 'task-cancel':
|
|
432
|
+
this.emit('signal', {
|
|
433
|
+
type: signal.type,
|
|
434
|
+
from: signal.from,
|
|
435
|
+
data: signal.data,
|
|
436
|
+
verified: !!signal.signature,
|
|
437
|
+
signature: signal.signature,
|
|
438
|
+
publicKey: signal.publicKey,
|
|
439
|
+
timestamp: signal.timestamp,
|
|
440
|
+
});
|
|
441
|
+
break;
|
|
426
442
|
default:
|
|
427
443
|
this.emit('signal', { ...signal, verified: !!signal.signature });
|
|
428
444
|
}
|
|
@@ -449,6 +465,42 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
449
465
|
return this.sendSignal(toPeerId, 'ice-candidate', candidate);
|
|
450
466
|
}
|
|
451
467
|
|
|
468
|
+
/**
|
|
469
|
+
* Serialize WebRTC objects to plain JSON for Firebase storage
|
|
470
|
+
* RTCIceCandidate and RTCSessionDescription are not directly storable
|
|
471
|
+
*/
|
|
472
|
+
_serializeWebRTCData(data) {
|
|
473
|
+
if (!data || typeof data !== 'object') {
|
|
474
|
+
return data;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Handle RTCIceCandidate
|
|
478
|
+
if (data.candidate !== undefined && data.sdpMid !== undefined) {
|
|
479
|
+
return {
|
|
480
|
+
candidate: data.candidate,
|
|
481
|
+
sdpMid: data.sdpMid,
|
|
482
|
+
sdpMLineIndex: data.sdpMLineIndex,
|
|
483
|
+
usernameFragment: data.usernameFragment || null,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Handle RTCSessionDescription (offer/answer)
|
|
488
|
+
if (data.type !== undefined && data.sdp !== undefined) {
|
|
489
|
+
return {
|
|
490
|
+
type: data.type,
|
|
491
|
+
sdp: data.sdp,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Try to convert any object with toJSON method
|
|
496
|
+
if (typeof data.toJSON === 'function') {
|
|
497
|
+
return data.toJSON();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Return as-is if already plain object
|
|
501
|
+
return data;
|
|
502
|
+
}
|
|
503
|
+
|
|
452
504
|
/**
|
|
453
505
|
* Send signal via Firebase with WASM signature
|
|
454
506
|
*/
|
|
@@ -462,12 +514,15 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
462
514
|
const signalId = `${this.peerId}-${toPeerId}-${Date.now()}`;
|
|
463
515
|
const signalRef = doc(this.db, SIGNALING_PATHS.signals, signalId);
|
|
464
516
|
|
|
517
|
+
// Serialize WebRTC objects to plain JSON
|
|
518
|
+
const serializedData = this._serializeWebRTCData(data);
|
|
519
|
+
|
|
465
520
|
const timestamp = Date.now();
|
|
466
521
|
const signalData = {
|
|
467
522
|
from: this.peerId,
|
|
468
523
|
to: toPeerId,
|
|
469
524
|
type,
|
|
470
|
-
data,
|
|
525
|
+
data: serializedData,
|
|
471
526
|
timestamp,
|
|
472
527
|
room: this.room,
|
|
473
528
|
};
|
|
@@ -478,7 +533,7 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
478
533
|
from: this.peerId,
|
|
479
534
|
to: toPeerId,
|
|
480
535
|
type,
|
|
481
|
-
data: typeof
|
|
536
|
+
data: typeof serializedData === 'object' ? JSON.stringify(serializedData) : serializedData,
|
|
482
537
|
timestamp
|
|
483
538
|
});
|
|
484
539
|
signalData.signature = signed.signature;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ruvector/edge-net",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Distributed compute intelligence network with WASM cryptographic security - contribute browser compute, spawn distributed AI agents, earn credits. Features Ed25519 signing, PiKey identity, Time Crystal coordination, Neural DAG attention, P2P swarm intelligence, ONNX inference, WebRTC signaling, CRDT ledger, and multi-agent workflows.",
|
|
6
6
|
"main": "ruvector_edge_net.js",
|
|
@@ -77,6 +77,8 @@
|
|
|
77
77
|
"ruvector_edge_net.d.ts",
|
|
78
78
|
"ruvector_edge_net_bg.wasm.d.ts",
|
|
79
79
|
"node/",
|
|
80
|
+
"deploy/",
|
|
81
|
+
"tests/",
|
|
80
82
|
"index.js",
|
|
81
83
|
"cli.js",
|
|
82
84
|
"join.js",
|
|
@@ -189,6 +191,11 @@
|
|
|
189
191
|
"signaling": "node -e \"import('./signaling.js').then(m => new m.SignalingServer().start())\"",
|
|
190
192
|
"genesis": "node genesis.js",
|
|
191
193
|
"genesis:start": "node genesis.js --port 8787",
|
|
194
|
+
"genesis:prod": "NODE_ENV=production node deploy/genesis-prod.js",
|
|
195
|
+
"genesis:docker": "docker-compose -f deploy/docker-compose.yml up -d",
|
|
196
|
+
"genesis:docker:logs": "docker-compose -f deploy/docker-compose.yml logs -f genesis",
|
|
197
|
+
"genesis:docker:stop": "docker-compose -f deploy/docker-compose.yml down",
|
|
198
|
+
"genesis:docker:build": "docker build -t ruvector/edge-net-genesis:latest -f deploy/Dockerfile .",
|
|
192
199
|
"p2p": "node -e \"import('./p2p.js').then(m => m.createP2PNetwork({ nodeId: 'test' }))\"",
|
|
193
200
|
"monitor": "node -e \"import('./monitor.js').then(m => { const mon = new m.Monitor(); mon.start(); setInterval(() => console.log(JSON.stringify(mon.generateReport(), null, 2)), 5000); })\"",
|
|
194
201
|
"firebase:setup": "node firebase-setup.js",
|
package/real-workers.js
CHANGED
|
@@ -295,6 +295,7 @@ export class RealWorkerPool extends EventEmitter {
|
|
|
295
295
|
status: 'idle',
|
|
296
296
|
tasksCompleted: 0,
|
|
297
297
|
currentTask: null,
|
|
298
|
+
terminated: false, // Track intentional termination
|
|
298
299
|
};
|
|
299
300
|
|
|
300
301
|
worker.on('message', (msg) => {
|
|
@@ -307,13 +308,16 @@ export class RealWorkerPool extends EventEmitter {
|
|
|
307
308
|
});
|
|
308
309
|
|
|
309
310
|
worker.on('exit', (code) => {
|
|
310
|
-
if (
|
|
311
|
-
|
|
311
|
+
// Only respawn if worker crashed unexpectedly (not terminated intentionally)
|
|
312
|
+
if (!workerInfo.terminated && this.status === 'ready') {
|
|
313
|
+
console.error(`[Worker ${index}] Exited unexpectedly with code ${code}, respawning...`);
|
|
312
314
|
// Respawn worker
|
|
313
315
|
const idx = this.workers.indexOf(workerInfo);
|
|
314
|
-
if (idx >= 0
|
|
316
|
+
if (idx >= 0) {
|
|
315
317
|
this.spawnWorker(index).then(w => {
|
|
316
318
|
this.workers[idx] = w;
|
|
319
|
+
}).catch(err => {
|
|
320
|
+
console.error(`[Worker ${index}] Failed to respawn:`, err.message);
|
|
317
321
|
});
|
|
318
322
|
}
|
|
319
323
|
}
|
|
@@ -537,8 +541,9 @@ export class RealWorkerPool extends EventEmitter {
|
|
|
537
541
|
await new Promise(r => setTimeout(r, 100));
|
|
538
542
|
}
|
|
539
543
|
|
|
540
|
-
// Terminate workers
|
|
544
|
+
// Terminate workers (mark as intentionally terminated first)
|
|
541
545
|
for (const workerInfo of this.workers) {
|
|
546
|
+
workerInfo.terminated = true;
|
|
542
547
|
await workerInfo.worker.terminate();
|
|
543
548
|
}
|
|
544
549
|
|
package/scheduler.js
CHANGED
|
@@ -596,11 +596,15 @@ export class TaskScheduler extends EventEmitter {
|
|
|
596
596
|
worker.allocate(task);
|
|
597
597
|
this.running.set(task.id, task);
|
|
598
598
|
|
|
599
|
-
// Calculate wait time
|
|
599
|
+
// Calculate wait time using running average
|
|
600
600
|
const waitTime = task.startedAt - task.queuedAt;
|
|
601
|
-
this.stats.
|
|
602
|
-
|
|
603
|
-
|
|
601
|
+
const assignedCount = this.stats.completed + this.running.size;
|
|
602
|
+
if (assignedCount <= 1) {
|
|
603
|
+
this.stats.avgWaitTime = waitTime;
|
|
604
|
+
} else {
|
|
605
|
+
this.stats.avgWaitTime =
|
|
606
|
+
(this.stats.avgWaitTime * (assignedCount - 1) + waitTime) / assignedCount;
|
|
607
|
+
}
|
|
604
608
|
|
|
605
609
|
this.emit('task-assigned', {
|
|
606
610
|
taskId: task.id,
|