@klastra/ksync
Version:
Real-time sync engine for modern applications with 300k+ ops/sec performance
158 lines (157 loc) • 5.61 kB
JavaScript
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);
}