Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
546
vendor/ruvector/examples/edge-net/dashboard/src/services/edgeNet.ts
vendored
Normal file
546
vendor/ruvector/examples/edge-net/dashboard/src/services/edgeNet.ts
vendored
Normal file
@@ -0,0 +1,546 @@
|
||||
/**
|
||||
* EdgeNet Service - Real WASM Integration
|
||||
*
|
||||
* Provides real EdgeNetNode and PiKey functionality from the WASM module.
|
||||
* All operations are secure and use actual cryptographic primitives.
|
||||
*/
|
||||
|
||||
// Types from the WASM module
|
||||
export interface NodeStats {
|
||||
ruv_earned: bigint;
|
||||
ruv_spent: bigint;
|
||||
tasks_completed: bigint;
|
||||
tasks_submitted: bigint;
|
||||
uptime_seconds: bigint;
|
||||
reputation: number;
|
||||
multiplier: number;
|
||||
celebration_boost: number;
|
||||
}
|
||||
|
||||
export interface EdgeNetModule {
|
||||
default: (input?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module) => Promise<void>;
|
||||
PiKey: new (genesis_seed?: Uint8Array | null) => PiKeyInstance;
|
||||
EdgeNetNode: new (site_id: string, config?: NodeConfigInstance | null) => EdgeNetNodeInstance;
|
||||
EdgeNetConfig: new (site_id: string) => EdgeNetConfigInstance;
|
||||
BrowserFingerprint: { generate(): Promise<string> };
|
||||
AdaptiveSecurity: new () => AdaptiveSecurityInstance;
|
||||
TimeCrystal: new (frequency: number) => TimeCrystalInstance;
|
||||
}
|
||||
|
||||
export interface PiKeyInstance {
|
||||
free(): void;
|
||||
getIdentity(): Uint8Array;
|
||||
getIdentityHex(): string;
|
||||
getShortId(): string;
|
||||
getPublicKey(): Uint8Array;
|
||||
sign(data: Uint8Array): Uint8Array;
|
||||
verify(data: Uint8Array, signature: Uint8Array, public_key: Uint8Array): boolean;
|
||||
createEncryptedBackup(password: string): Uint8Array;
|
||||
exportCompact(): Uint8Array;
|
||||
getStats(): string;
|
||||
verifyPiMagic(): boolean;
|
||||
getGenesisFingerprint(): Uint8Array;
|
||||
}
|
||||
|
||||
export interface NodeConfigInstance {
|
||||
cpu_limit: number;
|
||||
memory_limit: number;
|
||||
bandwidth_limit: number;
|
||||
min_idle_time: number;
|
||||
respect_battery: boolean;
|
||||
}
|
||||
|
||||
export interface EdgeNetNodeInstance {
|
||||
free(): void;
|
||||
nodeId(): string;
|
||||
start(): void;
|
||||
pause(): void;
|
||||
resume(): void;
|
||||
disconnect(): void;
|
||||
isIdle(): boolean;
|
||||
creditBalance(): bigint;
|
||||
ruvBalance(): bigint;
|
||||
getStats(): NodeStats;
|
||||
getThrottle(): number;
|
||||
getMultiplier(): number;
|
||||
getTreasury(): bigint;
|
||||
getProtocolFund(): bigint;
|
||||
getMerkleRoot(): string;
|
||||
getNetworkFitness(): number;
|
||||
getTimeCrystalSync(): number;
|
||||
getConflictCount(): number;
|
||||
getQuarantinedCount(): number;
|
||||
getCoherenceEventCount(): number;
|
||||
getPatternCount(): number;
|
||||
getTrajectoryCount(): number;
|
||||
getFounderCount(): number;
|
||||
isStreamHealthy(): boolean;
|
||||
shouldReplicate(): boolean;
|
||||
submitTask(task_type: string, payload: Uint8Array, max_credits: bigint): Promise<unknown>;
|
||||
processNextTask(): Promise<boolean>;
|
||||
processEpoch(): void;
|
||||
enableTimeCrystal(oscillators: number): boolean;
|
||||
enableHDC(): boolean;
|
||||
enableNAO(quorum: number): boolean;
|
||||
enableWTA(num_neurons: number): boolean;
|
||||
enableBTSP(input_dim: number): boolean;
|
||||
enableMicroLoRA(rank: number): boolean;
|
||||
enableGlobalWorkspace(capacity: number): boolean;
|
||||
enableMorphogenetic(size: number): boolean;
|
||||
storePattern(pattern_json: string): number;
|
||||
lookupPatterns(query_json: string, k: number): string;
|
||||
prunePatterns(min_usage: number, min_confidence: number): number;
|
||||
recordLearningTrajectory(trajectory_json: string): boolean;
|
||||
recordPerformance(success_rate: number, throughput: number): void;
|
||||
recordTaskRouting(task_type: string, node_id: string, latency_ms: bigint, success: boolean): void;
|
||||
recordPeerInteraction(peer_id: string, success_rate: number): void;
|
||||
getOptimalPeers(count: number): string[];
|
||||
proposeNAO(action: string): string;
|
||||
voteNAO(proposal_id: string, weight: number): boolean;
|
||||
canUseClaim(claim_id: string): boolean;
|
||||
getClaimQuarantineLevel(claim_id: string): number;
|
||||
runSecurityAudit(): string;
|
||||
checkEvents(): string;
|
||||
getThemedStatus(node_count: number): string;
|
||||
getMotivation(): string;
|
||||
getCapabilities(): unknown;
|
||||
getCapabilitiesSummary(): unknown;
|
||||
getCoherenceStats(): string;
|
||||
getEconomicHealth(): string;
|
||||
getLearningStats(): string;
|
||||
getOptimizationStats(): string;
|
||||
getRecommendedConfig(): string;
|
||||
getEnergyEfficiency(seq_len: number, hidden_dim: number): number;
|
||||
isSelfSustaining(active_nodes: number, daily_tasks: bigint): boolean;
|
||||
stepCapabilities(dt: number): void;
|
||||
}
|
||||
|
||||
export interface EdgeNetConfigInstance {
|
||||
cpuLimit(limit: number): EdgeNetConfigInstance;
|
||||
memoryLimit(bytes: number): EdgeNetConfigInstance;
|
||||
minIdleTime(ms: number): EdgeNetConfigInstance;
|
||||
respectBattery(respect: boolean): EdgeNetConfigInstance;
|
||||
addRelay(url: string): EdgeNetConfigInstance;
|
||||
build(): EdgeNetNodeInstance;
|
||||
}
|
||||
|
||||
export interface AdaptiveSecurityInstance {
|
||||
free(): void;
|
||||
chooseAction(state: string, available_actions: string): string;
|
||||
detectAttack(features: Float32Array): number;
|
||||
exportPatterns(): Uint8Array;
|
||||
importPatterns(data: Uint8Array): void;
|
||||
getSecurityLevel(): number;
|
||||
getRateLimitMax(): number;
|
||||
getMinReputation(): number;
|
||||
getSpotCheckProbability(): number;
|
||||
recordAttackPattern(pattern_type: string, features: Float32Array, severity: number): void;
|
||||
updateNetworkHealth(active_nodes: number, suspicious_nodes: number, attacks_hour: number, false_positives: number, avg_response_ms: number): void;
|
||||
learn(state: string, action: string, reward: number, next_state: string): void;
|
||||
getStats(): string;
|
||||
}
|
||||
|
||||
export interface TimeCrystalInstance {
|
||||
free(): void;
|
||||
getPhase(): number;
|
||||
getCoherence(): number;
|
||||
step(dt: number): void;
|
||||
synchronize(other_phase: number): void;
|
||||
getStats(): string;
|
||||
}
|
||||
|
||||
// Singleton service
|
||||
class EdgeNetService {
|
||||
private module: EdgeNetModule | null = null;
|
||||
private node: EdgeNetNodeInstance | null = null;
|
||||
private piKey: PiKeyInstance | null = null;
|
||||
private security: AdaptiveSecurityInstance | null = null;
|
||||
private initialized = false;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
private startTime = Date.now();
|
||||
private siteId = 'edge-net-dashboard';
|
||||
|
||||
/**
|
||||
* Initialize the WASM module
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
if (this.initPromise) return this.initPromise;
|
||||
|
||||
this.initPromise = this._doInit();
|
||||
await this.initPromise;
|
||||
}
|
||||
|
||||
private async _doInit(): Promise<void> {
|
||||
try {
|
||||
console.log('[EdgeNet] Loading WASM module...');
|
||||
|
||||
// Try loading from the local package first (for development)
|
||||
let wasmModule: EdgeNetModule;
|
||||
|
||||
// Load from CDN - the package is published to npm
|
||||
try {
|
||||
const cdnUrl = 'https://unpkg.com/@ruvector/edge-net@0.1.1/ruvector_edge_net.js';
|
||||
wasmModule = await import(/* @vite-ignore */ cdnUrl) as unknown as EdgeNetModule;
|
||||
} catch (cdnError) {
|
||||
console.warn('[EdgeNet] CDN load failed, running in fallback mode:', cdnError);
|
||||
// Module load failed - will run in fallback mode
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize the WASM
|
||||
await wasmModule.default();
|
||||
this.module = wasmModule;
|
||||
|
||||
console.log('[EdgeNet] WASM module loaded successfully');
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
console.error('[EdgeNet] Failed to load WASM module:', error);
|
||||
// Set initialized to true but with null module - will use fallback mode
|
||||
this.initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WASM is available
|
||||
*/
|
||||
isWASMAvailable(): boolean {
|
||||
return this.module !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new PiKey identity
|
||||
*/
|
||||
async generateIdentity(seed?: Uint8Array): Promise<PiKeyInstance | null> {
|
||||
await this.init();
|
||||
|
||||
if (!this.module) {
|
||||
console.warn('[EdgeNet] WASM not available, using Web Crypto fallback');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
this.piKey = new this.module.PiKey(seed || null);
|
||||
console.log('[EdgeNet] Generated PiKey:', this.piKey.getShortId());
|
||||
return this.piKey;
|
||||
} catch (error) {
|
||||
console.error('[EdgeNet] Failed to generate PiKey:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current PiKey
|
||||
*/
|
||||
getPiKey(): PiKeyInstance | null {
|
||||
return this.piKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and start an EdgeNet node
|
||||
*/
|
||||
async createNode(siteId?: string): Promise<EdgeNetNodeInstance | null> {
|
||||
await this.init();
|
||||
|
||||
if (!this.module) {
|
||||
console.warn('[EdgeNet] WASM not available');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const id = siteId || this.siteId;
|
||||
|
||||
// Use config builder for customization
|
||||
const config = new this.module.EdgeNetConfig(id)
|
||||
.addRelay('wss://edge-net-relay-875130704813.us-central1.run.app') // Genesis relay
|
||||
.cpuLimit(0.5) // 50% CPU when idle
|
||||
.memoryLimit(512 * 1024 * 1024) // 512MB
|
||||
.minIdleTime(5000) // 5 seconds idle before contributing
|
||||
.respectBattery(true);
|
||||
|
||||
this.node = config.build();
|
||||
console.log('[EdgeNet] Node created:', this.node.nodeId());
|
||||
|
||||
return this.node;
|
||||
} catch (error) {
|
||||
console.error('[EdgeNet] Failed to create node:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current node
|
||||
*/
|
||||
getNode(): EdgeNetNodeInstance | null {
|
||||
return this.node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the node
|
||||
*/
|
||||
startNode(): void {
|
||||
if (this.node) {
|
||||
this.node.start();
|
||||
// Enable all capabilities for maximum earning
|
||||
this.node.enableTimeCrystal(8);
|
||||
this.node.enableHDC();
|
||||
this.node.enableWTA(64);
|
||||
console.log('[EdgeNet] Node started with full capabilities');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the node
|
||||
*/
|
||||
pauseNode(): void {
|
||||
if (this.node) {
|
||||
this.node.pause();
|
||||
console.log('[EdgeNet] Node paused');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume the node
|
||||
*/
|
||||
resumeNode(): void {
|
||||
if (this.node) {
|
||||
this.node.resume();
|
||||
console.log('[EdgeNet] Node resumed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an epoch - advances time and accumulates rewards
|
||||
*/
|
||||
processEpoch(): void {
|
||||
if (this.node) {
|
||||
this.node.processEpoch();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step capabilities forward (for real-time updates)
|
||||
*/
|
||||
stepCapabilities(dt: number): void {
|
||||
if (this.node) {
|
||||
this.node.stepCapabilities(dt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record performance for learning
|
||||
*/
|
||||
recordPerformance(successRate: number, throughput: number): void {
|
||||
if (this.node) {
|
||||
this.node.recordPerformance(successRate, throughput);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get real node statistics
|
||||
*/
|
||||
getStats(): NodeStats | null {
|
||||
if (!this.node) return null;
|
||||
|
||||
try {
|
||||
return this.node.getStats();
|
||||
} catch (error) {
|
||||
console.error('[EdgeNet] Failed to get stats:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get credit balance
|
||||
*/
|
||||
getCreditBalance(): bigint {
|
||||
if (!this.node) return BigInt(0);
|
||||
return this.node.creditBalance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Time Crystal synchronization level
|
||||
*/
|
||||
getTimeCrystalSync(): number {
|
||||
if (!this.node) return 0;
|
||||
return this.node.getTimeCrystalSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable Time Crystal
|
||||
*/
|
||||
enableTimeCrystal(oscillators = 8): boolean {
|
||||
if (!this.node) return false;
|
||||
return this.node.enableTimeCrystal(oscillators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get network fitness score
|
||||
*/
|
||||
getNetworkFitness(): number {
|
||||
if (!this.node) return 0;
|
||||
return this.node.getNetworkFitness();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize adaptive security
|
||||
*/
|
||||
async initSecurity(): Promise<AdaptiveSecurityInstance | null> {
|
||||
await this.init();
|
||||
|
||||
if (!this.module) return null;
|
||||
|
||||
try {
|
||||
this.security = new this.module.AdaptiveSecurity();
|
||||
console.log('[EdgeNet] Adaptive security initialized');
|
||||
return this.security;
|
||||
} catch (error) {
|
||||
console.error('[EdgeNet] Failed to init security:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get security level
|
||||
*/
|
||||
getSecurityLevel(): number {
|
||||
if (!this.security) return 0;
|
||||
return this.security.getSecurityLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run security audit
|
||||
*/
|
||||
runSecurityAudit(): string | null {
|
||||
if (!this.node) return null;
|
||||
return this.node.runSecurityAudit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get browser fingerprint for unique node identification
|
||||
*/
|
||||
async getBrowserFingerprint(): Promise<string | null> {
|
||||
await this.init();
|
||||
|
||||
if (!this.module) return null;
|
||||
|
||||
try {
|
||||
return await this.module.BrowserFingerprint.generate();
|
||||
} catch (error) {
|
||||
console.error('[EdgeNet] Failed to generate fingerprint:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get economic health metrics
|
||||
*/
|
||||
getEconomicHealth(): string | null {
|
||||
if (!this.node) return null;
|
||||
return this.node.getEconomicHealth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get learning statistics
|
||||
*/
|
||||
getLearningStats(): string | null {
|
||||
if (!this.node) return null;
|
||||
return this.node.getLearningStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a learning pattern
|
||||
*/
|
||||
storePattern(pattern: object): number {
|
||||
if (!this.node) return -1;
|
||||
return this.node.storePattern(JSON.stringify(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup similar patterns
|
||||
*/
|
||||
lookupPatterns(query: object, k = 5): unknown[] {
|
||||
if (!this.node) return [];
|
||||
try {
|
||||
const result = this.node.lookupPatterns(JSON.stringify(query), k);
|
||||
return JSON.parse(result);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a task to the network
|
||||
*/
|
||||
async submitTask(taskType: string, payload: Uint8Array, maxCredits: bigint): Promise<unknown> {
|
||||
if (!this.node) throw new Error('Node not initialized');
|
||||
return this.node.submitTask(taskType, payload, maxCredits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a demo compute task (for earning credits in demo mode)
|
||||
*/
|
||||
async submitDemoTask(): Promise<void> {
|
||||
if (!this.node) return;
|
||||
try {
|
||||
// Submit a small compute task
|
||||
const payload = new TextEncoder().encode(JSON.stringify({
|
||||
type: 'compute',
|
||||
data: Math.random().toString(36),
|
||||
timestamp: Date.now(),
|
||||
}));
|
||||
await this.node.submitTask('compute', payload, BigInt(1000000)); // 0.001 rUv max
|
||||
} catch {
|
||||
// Task submission can fail if queue is full - that's ok
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the next available task
|
||||
*/
|
||||
async processNextTask(): Promise<boolean> {
|
||||
if (!this.node) return false;
|
||||
return this.node.processNextTask();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get capabilities summary
|
||||
*/
|
||||
getCapabilities(): unknown {
|
||||
if (!this.node) return null;
|
||||
return this.node.getCapabilitiesSummary();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get uptime in seconds
|
||||
*/
|
||||
getUptime(): number {
|
||||
return (Date.now() - this.startTime) / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.node) {
|
||||
this.node.disconnect();
|
||||
this.node.free();
|
||||
this.node = null;
|
||||
}
|
||||
if (this.piKey) {
|
||||
this.piKey.free();
|
||||
this.piKey = null;
|
||||
}
|
||||
if (this.security) {
|
||||
this.security.free();
|
||||
this.security = null;
|
||||
}
|
||||
console.log('[EdgeNet] Service destroyed');
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const edgeNetService = new EdgeNetService();
|
||||
|
||||
// Export types for external use
|
||||
export type { EdgeNetService };
|
||||
394
vendor/ruvector/examples/edge-net/dashboard/src/services/relayClient.ts
vendored
Normal file
394
vendor/ruvector/examples/edge-net/dashboard/src/services/relayClient.ts
vendored
Normal file
@@ -0,0 +1,394 @@
|
||||
/**
|
||||
* Edge-Net Relay WebSocket Client
|
||||
*
|
||||
* Provides real-time connection to the Edge-Net relay server for:
|
||||
* - Node registration and presence
|
||||
* - Task distribution and completion
|
||||
* - Credit synchronization
|
||||
* - Time Crystal phase sync
|
||||
*/
|
||||
|
||||
export interface RelayMessage {
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface NetworkState {
|
||||
genesisTime: number;
|
||||
totalNodes: number;
|
||||
activeNodes: number;
|
||||
totalTasks: number;
|
||||
totalRuvDistributed: bigint;
|
||||
timeCrystalPhase: number;
|
||||
}
|
||||
|
||||
export interface TaskAssignment {
|
||||
id: string;
|
||||
submitter: string;
|
||||
taskType: string;
|
||||
payload: Uint8Array;
|
||||
maxCredits: bigint;
|
||||
submittedAt: number;
|
||||
}
|
||||
|
||||
export interface RelayEventHandlers {
|
||||
onConnected?: (nodeId: string, networkState: NetworkState, peers: string[]) => void;
|
||||
onDisconnected?: () => void;
|
||||
onNodeJoined?: (nodeId: string, totalNodes: number) => void;
|
||||
onNodeLeft?: (nodeId: string, totalNodes: number) => void;
|
||||
onTaskAssigned?: (task: TaskAssignment) => void;
|
||||
onTaskResult?: (taskId: string, result: unknown, processedBy: string) => void;
|
||||
onCreditEarned?: (amount: bigint, taskId: string) => void;
|
||||
onTimeCrystalSync?: (phase: number, timestamp: number, activeNodes: number) => void;
|
||||
onPeerMessage?: (from: string, payload: unknown) => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
const RECONNECT_DELAYS = [1000, 2000, 5000, 10000, 30000]; // Exponential backoff
|
||||
|
||||
class RelayClient {
|
||||
private ws: WebSocket | null = null;
|
||||
private nodeId: string | null = null;
|
||||
private relayUrl: string;
|
||||
private handlers: RelayEventHandlers = {};
|
||||
private reconnectAttempt = 0;
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private isConnecting = false;
|
||||
private shouldReconnect = true;
|
||||
|
||||
constructor(relayUrl: string = 'wss://edge-net-relay-875130704813.us-central1.run.app') {
|
||||
this.relayUrl = relayUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set event handlers
|
||||
*/
|
||||
setHandlers(handlers: RelayEventHandlers): void {
|
||||
this.handlers = { ...this.handlers, ...handlers };
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the relay server
|
||||
*/
|
||||
async connect(nodeId: string): Promise<boolean> {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
console.log('[RelayClient] Already connected');
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.isConnecting) {
|
||||
console.log('[RelayClient] Connection already in progress');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.nodeId = nodeId;
|
||||
this.shouldReconnect = true;
|
||||
this.isConnecting = true;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
console.log(`[RelayClient] Connecting to ${this.relayUrl}...`);
|
||||
this.ws = new WebSocket(this.relayUrl);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('[RelayClient] WebSocket connected');
|
||||
this.isConnecting = false;
|
||||
this.reconnectAttempt = 0;
|
||||
|
||||
// Register with relay
|
||||
this.send({
|
||||
type: 'register',
|
||||
nodeId: this.nodeId,
|
||||
capabilities: ['compute', 'storage'],
|
||||
version: '0.1.0',
|
||||
});
|
||||
|
||||
// Start heartbeat
|
||||
this.startHeartbeat();
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
this.handleMessage(event.data);
|
||||
};
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
console.log(`[RelayClient] WebSocket closed: ${event.code} ${event.reason}`);
|
||||
this.isConnecting = false;
|
||||
this.stopHeartbeat();
|
||||
this.handlers.onDisconnected?.();
|
||||
|
||||
if (this.shouldReconnect) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('[RelayClient] WebSocket error:', error);
|
||||
this.isConnecting = false;
|
||||
this.handlers.onError?.(new Error('WebSocket connection failed'));
|
||||
resolve(false);
|
||||
};
|
||||
|
||||
// Wait for welcome message to confirm connection
|
||||
const checkConnected = setInterval(() => {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
clearInterval(checkConnected);
|
||||
resolve(true);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Timeout after 10 seconds
|
||||
setTimeout(() => {
|
||||
clearInterval(checkConnected);
|
||||
if (this.ws?.readyState !== WebSocket.OPEN) {
|
||||
this.isConnecting = false;
|
||||
resolve(false);
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[RelayClient] Failed to create WebSocket:', error);
|
||||
this.isConnecting = false;
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the relay
|
||||
*/
|
||||
disconnect(): void {
|
||||
this.shouldReconnect = false;
|
||||
this.stopHeartbeat();
|
||||
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
|
||||
if (this.ws) {
|
||||
this.ws.close(1000, 'Client disconnect');
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
console.log('[RelayClient] Disconnected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if connected
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return this.ws?.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current node ID
|
||||
*/
|
||||
getNodeId(): string | null {
|
||||
return this.nodeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a task to the network
|
||||
*/
|
||||
submitTask(taskType: string, payload: Uint8Array, maxCredits: bigint): void {
|
||||
this.send({
|
||||
type: 'task_submit',
|
||||
task: {
|
||||
taskType,
|
||||
payload: Array.from(payload), // Convert to array for JSON
|
||||
maxCredits: maxCredits.toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Report task completion
|
||||
*/
|
||||
completeTask(taskId: string, submitterId: string, result: unknown, reward: bigint): void {
|
||||
this.send({
|
||||
type: 'task_complete',
|
||||
taskId,
|
||||
submitterId,
|
||||
result,
|
||||
reward: reward.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to a specific peer
|
||||
*/
|
||||
sendToPeer(targetId: string, payload: unknown): void {
|
||||
this.send({
|
||||
type: 'peer_message',
|
||||
targetId,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast a message to all peers
|
||||
*/
|
||||
broadcast(payload: unknown): void {
|
||||
this.send({
|
||||
type: 'broadcast',
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Private Methods
|
||||
// ============================================================================
|
||||
|
||||
private send(message: RelayMessage): void {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify(message));
|
||||
} else {
|
||||
console.warn('[RelayClient] Cannot send - not connected');
|
||||
}
|
||||
}
|
||||
|
||||
private handleMessage(data: string): void {
|
||||
try {
|
||||
const message = JSON.parse(data) as RelayMessage;
|
||||
|
||||
switch (message.type) {
|
||||
case 'welcome':
|
||||
console.log('[RelayClient] Registered with relay:', message.nodeId);
|
||||
this.handlers.onConnected?.(
|
||||
message.nodeId as string,
|
||||
{
|
||||
genesisTime: (message.networkState as NetworkState)?.genesisTime || Date.now(),
|
||||
totalNodes: (message.networkState as NetworkState)?.totalNodes || 0,
|
||||
activeNodes: (message.networkState as NetworkState)?.activeNodes || 0,
|
||||
totalTasks: (message.networkState as NetworkState)?.totalTasks || 0,
|
||||
totalRuvDistributed: BigInt((message.networkState as NetworkState)?.totalRuvDistributed?.toString() || '0'),
|
||||
timeCrystalPhase: (message.networkState as NetworkState)?.timeCrystalPhase || 0,
|
||||
},
|
||||
(message.peers as string[]) || []
|
||||
);
|
||||
break;
|
||||
|
||||
case 'node_joined':
|
||||
console.log('[RelayClient] Node joined:', message.nodeId);
|
||||
this.handlers.onNodeJoined?.(
|
||||
message.nodeId as string,
|
||||
message.totalNodes as number
|
||||
);
|
||||
break;
|
||||
|
||||
case 'node_left':
|
||||
console.log('[RelayClient] Node left:', message.nodeId);
|
||||
this.handlers.onNodeLeft?.(
|
||||
message.nodeId as string,
|
||||
message.totalNodes as number
|
||||
);
|
||||
break;
|
||||
|
||||
case 'task_assignment':
|
||||
console.log('[RelayClient] Task assigned:', (message.task as TaskAssignment)?.id);
|
||||
const task = message.task as Record<string, unknown>;
|
||||
this.handlers.onTaskAssigned?.({
|
||||
id: task.id as string,
|
||||
submitter: task.submitter as string,
|
||||
taskType: task.taskType as string,
|
||||
payload: new Uint8Array(task.payload as number[]),
|
||||
maxCredits: BigInt(task.maxCredits as string || '0'),
|
||||
submittedAt: task.submittedAt as number,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'task_accepted':
|
||||
console.log('[RelayClient] Task accepted:', message.taskId);
|
||||
break;
|
||||
|
||||
case 'task_result':
|
||||
console.log('[RelayClient] Task result:', message.taskId);
|
||||
this.handlers.onTaskResult?.(
|
||||
message.taskId as string,
|
||||
message.result,
|
||||
message.processedBy as string
|
||||
);
|
||||
break;
|
||||
|
||||
case 'credit_earned':
|
||||
console.log('[RelayClient] Credit earned:', message.amount);
|
||||
this.handlers.onCreditEarned?.(
|
||||
BigInt(message.amount as string || '0'),
|
||||
message.taskId as string
|
||||
);
|
||||
break;
|
||||
|
||||
case 'time_crystal_sync':
|
||||
this.handlers.onTimeCrystalSync?.(
|
||||
message.phase as number,
|
||||
message.timestamp as number,
|
||||
message.activeNodes as number
|
||||
);
|
||||
break;
|
||||
|
||||
case 'peer_message':
|
||||
this.handlers.onPeerMessage?.(
|
||||
message.from as string,
|
||||
message.payload
|
||||
);
|
||||
break;
|
||||
|
||||
case 'heartbeat_ack':
|
||||
// Heartbeat acknowledged
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error('[RelayClient] Relay error:', message.message);
|
||||
this.handlers.onError?.(new Error(message.message as string));
|
||||
break;
|
||||
|
||||
case 'relay_shutdown':
|
||||
console.warn('[RelayClient] Relay is shutting down');
|
||||
this.shouldReconnect = true; // Will reconnect when relay comes back
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('[RelayClient] Unknown message type:', message.type);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[RelayClient] Failed to parse message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private startHeartbeat(): void {
|
||||
this.stopHeartbeat();
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
this.send({ type: 'heartbeat' });
|
||||
}, 15000); // Every 15 seconds
|
||||
}
|
||||
|
||||
private stopHeartbeat(): void {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer);
|
||||
this.heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
if (this.reconnectTimer) return;
|
||||
|
||||
const delay = RECONNECT_DELAYS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS.length - 1)];
|
||||
console.log(`[RelayClient] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt + 1})`);
|
||||
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectTimer = null;
|
||||
this.reconnectAttempt++;
|
||||
if (this.nodeId) {
|
||||
this.connect(this.nodeId);
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const relayClient = new RelayClient();
|
||||
|
||||
// Export class for testing
|
||||
export { RelayClient };
|
||||
152
vendor/ruvector/examples/edge-net/dashboard/src/services/storage.ts
vendored
Normal file
152
vendor/ruvector/examples/edge-net/dashboard/src/services/storage.ts
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* IndexedDB Storage Service
|
||||
* Persistent storage for Edge-Net node state
|
||||
*/
|
||||
|
||||
const DB_NAME = 'edge-net-db';
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = 'node-state';
|
||||
|
||||
interface NodeState {
|
||||
id: string;
|
||||
nodeId: string | null;
|
||||
creditsEarned: number;
|
||||
creditsSpent: number;
|
||||
tasksCompleted: number;
|
||||
tasksSubmitted: number;
|
||||
totalUptime: number;
|
||||
lastActiveTimestamp: number;
|
||||
consentGiven: boolean;
|
||||
consentTimestamp: number | null;
|
||||
cpuLimit: number;
|
||||
gpuEnabled: boolean;
|
||||
gpuLimit: number;
|
||||
respectBattery: boolean;
|
||||
onlyWhenIdle: boolean;
|
||||
}
|
||||
|
||||
class StorageService {
|
||||
private db: IDBDatabase | null = null;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (this.db) return;
|
||||
if (this.initPromise) return this.initPromise;
|
||||
|
||||
this.initPromise = new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onerror = () => {
|
||||
console.error('[Storage] Failed to open IndexedDB:', request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
console.log('[Storage] IndexedDB opened successfully');
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
|
||||
console.log('[Storage] Created object store:', STORE_NAME);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
async saveState(state: NodeState): Promise<void> {
|
||||
await this.init();
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.put(state);
|
||||
|
||||
request.onsuccess = () => {
|
||||
console.log('[Storage] State saved:', state.creditsEarned, 'rUv');
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error('[Storage] Failed to save state:', request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async loadState(): Promise<NodeState | null> {
|
||||
await this.init();
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.get('primary');
|
||||
|
||||
request.onsuccess = () => {
|
||||
const state = request.result as NodeState | undefined;
|
||||
if (state) {
|
||||
console.log('[Storage] Loaded state:', state.creditsEarned, 'rUv earned');
|
||||
} else {
|
||||
console.log('[Storage] No saved state found');
|
||||
}
|
||||
resolve(state || null);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error('[Storage] Failed to load state:', request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getDefaultState(): Promise<NodeState> {
|
||||
return {
|
||||
id: 'primary',
|
||||
nodeId: null,
|
||||
creditsEarned: 0,
|
||||
creditsSpent: 0,
|
||||
tasksCompleted: 0,
|
||||
tasksSubmitted: 0,
|
||||
totalUptime: 0,
|
||||
lastActiveTimestamp: Date.now(),
|
||||
consentGiven: false,
|
||||
consentTimestamp: null,
|
||||
cpuLimit: 50,
|
||||
gpuEnabled: false,
|
||||
gpuLimit: 30,
|
||||
respectBattery: true,
|
||||
onlyWhenIdle: true,
|
||||
};
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
await this.init();
|
||||
if (!this.db) return;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.clear();
|
||||
|
||||
request.onsuccess = () => {
|
||||
console.log('[Storage] State cleared');
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const storageService = new StorageService();
|
||||
export type { NodeState };
|
||||
Reference in New Issue
Block a user