synor/apps/explorer-web/src/lib/websocket.ts
Gulshan Yadav 48949ebb3f Initial commit: Synor blockchain monorepo
A complete blockchain implementation featuring:
- synord: Full node with GHOSTDAG consensus
- explorer-web: Modern React blockchain explorer with 3D DAG visualization
- CLI wallet and tools
- Smart contract SDK and example contracts (DEX, NFT, token)
- WASM crypto library for browser/mobile
2026-01-08 05:22:17 +05:30

275 lines
7.5 KiB
TypeScript

/**
* WebSocket service for real-time blockchain updates.
* Handles connection management, reconnection, and event subscriptions.
*/
export type WebSocketStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
export interface BlockEvent {
type: 'new_block';
hash: string;
blueScore: number;
timestamp: number;
txCount: number;
isChainBlock: boolean;
}
export interface StatsEvent {
type: 'stats_update';
blockCount: number;
virtualDaaScore: number;
difficulty: number;
mempoolSize: number;
hashrate: number;
hashrateHuman: string;
}
export interface TipEvent {
type: 'tip_update';
tips: string[];
tipCount: number;
}
export interface MempoolEvent {
type: 'mempool_tx';
txId: string;
fee: number;
mass: number;
}
export type WebSocketEvent = BlockEvent | StatsEvent | TipEvent | MempoolEvent;
type EventCallback<T extends WebSocketEvent = WebSocketEvent> = (event: T) => void;
type StatusCallback = (status: WebSocketStatus) => void;
class WebSocketService {
private ws: WebSocket | null = null;
private url: string;
private subscriptions: Map<string, Set<EventCallback>> = new Map();
private statusListeners: Set<StatusCallback> = new Set();
private status: WebSocketStatus = 'disconnected';
private reconnectAttempts = 0;
private maxReconnectAttempts = 10;
private reconnectDelay = 1000;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private mockMode = false;
private mockInterval: ReturnType<typeof setInterval> | null = null;
constructor() {
// Default WebSocket URL - can be overridden
const wsProtocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsHost = typeof window !== 'undefined' ? window.location.host : 'localhost:3000';
this.url = `${wsProtocol}//${wsHost}/ws`;
// Check if mock mode should be enabled
this.mockMode = this.shouldUseMock();
}
private shouldUseMock(): boolean {
if (typeof window !== 'undefined') {
if (localStorage.getItem('useMockApi') === 'true') return true;
}
return import.meta.env.VITE_USE_MOCK === 'true';
}
setUrl(url: string) {
this.url = url;
}
enableMock(enable: boolean) {
this.mockMode = enable;
if (enable && this.status === 'connected') {
this.startMockUpdates();
} else if (!enable) {
this.stopMockUpdates();
}
}
connect(): void {
if (this.mockMode) {
this.setStatus('connected');
this.startMockUpdates();
return;
}
if (this.ws?.readyState === WebSocket.OPEN) {
return;
}
this.setStatus('connecting');
try {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.setStatus('connected');
this.reconnectAttempts = 0;
this.reconnectDelay = 1000;
console.log('[WS] Connected to', this.url);
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data) as WebSocketEvent;
this.emit(data);
} catch (e) {
console.error('[WS] Failed to parse message:', e);
}
};
this.ws.onerror = (error) => {
console.error('[WS] Error:', error);
};
this.ws.onclose = (event) => {
console.log('[WS] Disconnected:', event.code, event.reason);
this.setStatus('disconnected');
this.scheduleReconnect();
};
} catch (error) {
console.error('[WS] Failed to connect:', error);
this.setStatus('disconnected');
this.scheduleReconnect();
}
}
disconnect(): void {
this.stopMockUpdates();
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
this.ws.close(1000, 'Client disconnect');
this.ws = null;
}
this.setStatus('disconnected');
}
private scheduleReconnect(): void {
if (this.mockMode) return;
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log('[WS] Max reconnect attempts reached');
return;
}
this.reconnectAttempts++;
this.setStatus('reconnecting');
const delay = Math.min(this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1), 30000);
console.log(`[WS] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
this.reconnectTimer = setTimeout(() => {
this.connect();
}, delay);
}
private setStatus(status: WebSocketStatus): void {
this.status = status;
this.statusListeners.forEach(callback => callback(status));
}
getStatus(): WebSocketStatus {
return this.status;
}
onStatusChange(callback: StatusCallback): () => void {
this.statusListeners.add(callback);
return () => this.statusListeners.delete(callback);
}
subscribe<T extends WebSocketEvent>(eventType: T['type'], callback: EventCallback<T>): () => void {
if (!this.subscriptions.has(eventType)) {
this.subscriptions.set(eventType, new Set());
}
this.subscriptions.get(eventType)!.add(callback as EventCallback);
// Return unsubscribe function
return () => {
this.subscriptions.get(eventType)?.delete(callback as EventCallback);
};
}
private emit(event: WebSocketEvent): void {
const callbacks = this.subscriptions.get(event.type);
if (callbacks) {
callbacks.forEach(callback => callback(event));
}
// Also emit to wildcard subscribers
const wildcardCallbacks = this.subscriptions.get('*');
if (wildcardCallbacks) {
wildcardCallbacks.forEach(callback => callback(event));
}
}
// Mock data simulation for development
private startMockUpdates(): void {
if (this.mockInterval) return;
let blockCounter = 125847;
let mempoolSize = 42;
this.mockInterval = setInterval(() => {
// Simulate new block every 1 second
blockCounter++;
const blockEvent: BlockEvent = {
type: 'new_block',
hash: this.generateMockHash(blockCounter),
blueScore: blockCounter,
timestamp: Date.now(),
txCount: Math.floor(Math.random() * 50) + 1,
isChainBlock: true,
};
this.emit(blockEvent);
// Stats update with each block
mempoolSize = Math.max(0, mempoolSize + Math.floor(Math.random() * 10) - 5);
const statsEvent: StatsEvent = {
type: 'stats_update',
blockCount: blockCounter,
virtualDaaScore: blockCounter,
difficulty: 1234567890 + Math.random() * 100000000,
mempoolSize,
hashrate: 45.6e9 + Math.random() * 5e9,
hashrateHuman: `${(45.6 + Math.random() * 5).toFixed(2)} GH/s`,
};
this.emit(statsEvent);
// Occasional tip updates
if (Math.random() > 0.7) {
const tipEvent: TipEvent = {
type: 'tip_update',
tips: [
this.generateMockHash(blockCounter),
this.generateMockHash(blockCounter - 1),
this.generateMockHash(blockCounter - 2),
],
tipCount: 3,
};
this.emit(tipEvent);
}
}, 1000);
}
private stopMockUpdates(): void {
if (this.mockInterval) {
clearInterval(this.mockInterval);
this.mockInterval = null;
}
}
private generateMockHash(seed: number): string {
const chars = '0123456789abcdef';
let hash = '';
for (let i = 0; i < 64; i++) {
const charIndex = (seed * 31 + i * 7 + Math.floor(i / 8) * seed) % 16;
hash += chars[Math.abs(charIndex)];
}
return hash;
}
}
// Singleton instance
export const wsService = new WebSocketService();
export default wsService;