UNPKG

@klastra/ksync

Version:

Real-time sync engine for modern applications with 300k+ ops/sec performance

158 lines (157 loc) 5.61 kB
import { WebSocketServer, WebSocket } from 'ws'; export class KSyncServer { port; wss; clients = new Map(); events = []; currentVersion = 0; heartbeatInterval; constructor(port = 8080) { this.port = port; this.wss = new WebSocketServer({ port }); this.setupServer(); this.startHeartbeat(); } setupServer() { console.log(`kSync server starting on port ${this.port}`); this.wss.on('connection', (ws) => { const clientId = this.generateClientId(); const client = { ws, id: clientId, lastSeen: Date.now() }; this.clients.set(clientId, client); console.log(`Client connected: ${clientId}`); ws.on('message', async (data) => { try { const message = JSON.parse(data.toString()); await this.handleMessage(client, message); } catch (error) { console.error('Error handling message:', error); } }); ws.on('close', () => { this.clients.delete(clientId); console.log(`Client disconnected: ${clientId}`); }); ws.on('error', (error) => { console.error(`Client error (${clientId}):`, error); this.clients.delete(clientId); }); // Send initial sync this.sendSyncResponse(client, 0); }); this.wss.on('error', (error) => { console.error('WebSocket server error:', error); }); } async handleMessage(client, message) { client.lastSeen = Date.now(); switch (message.type) { case 'event': await this.handleEvent(client, message.data); break; case 'sync-request': await this.handleSyncRequest(client, message.data); break; case 'pong': // Client is alive break; default: console.warn(`Unknown message type: ${message.type}`); } } async handleEvent(sender, event) { // Assign server version event.version = ++this.currentVersion; // Store event this.events.push(event); // Keep only last 10000 events to prevent memory issues if (this.events.length > 10000) { this.events = this.events.slice(-10000); } console.log(`Event received: ${event.type} from ${sender.id}`); // Broadcast to all other clients const message = { type: 'event', data: event }; this.broadcast(JSON.stringify(message), sender.id); } async handleSyncRequest(client, data) { console.log(`Sync request from ${client.id}, fromVersion: ${data.fromVersion}`); this.sendSyncResponse(client, data.fromVersion); } sendSyncResponse(client, fromVersion) { const eventsToSync = this.events.filter(event => event.version > fromVersion); if (eventsToSync.length > 0) { const message = { type: 'sync-response', data: eventsToSync }; client.ws.send(JSON.stringify(message)); console.log(`Sent ${eventsToSync.length} events to ${client.id}`); } } broadcast(message, excludeClientId) { this.clients.forEach((client, clientId) => { if (clientId !== excludeClientId && client.ws.readyState === WebSocket.OPEN) { client.ws.send(message); } }); } startHeartbeat() { this.heartbeatInterval = setInterval(() => { const now = Date.now(); const pingMessage = { type: 'ping' }; const pingData = JSON.stringify(pingMessage); this.clients.forEach((client, clientId) => { // Remove stale clients (no pong for 30 seconds) if (now - client.lastSeen > 30000) { console.log(`Removing stale client: ${clientId}`); client.ws.terminate(); this.clients.delete(clientId); return; } // Send ping if (client.ws.readyState === WebSocket.OPEN) { client.ws.send(pingData); } }); }, 10000); // Every 10 seconds } generateClientId() { return `server-client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } getStats() { return { connectedClients: this.clients.size, totalEvents: this.events.length, currentVersion: this.currentVersion }; } close() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); } this.wss.close(); console.log('kSync server closed'); } } // CLI usage (ESM compatible) if (import.meta.url === `file://${process.argv[1]}`) { const port = parseInt(process.env.PORT || '8080'); const server = new KSyncServer(port); process.on('SIGINT', () => { console.log('\nShutting down server...'); server.close(); process.exit(0); }); // Log stats every 30 seconds setInterval(() => { const stats = server.getStats(); console.log(`Stats: ${stats.connectedClients} clients, ${stats.totalEvents} events, version ${stats.currentVersion}`); }, 30000); }