@klastra/ksync
Version:
Real-time sync engine for modern applications with 300k+ ops/sec performance
366 lines (365 loc) • 12.2 kB
JavaScript
import { WebSocketServer, WebSocket } from 'ws';
import { createServer } from 'http';
import { EventEmitter } from 'events';
export class KSyncServer extends EventEmitter {
wss;
server;
clients = new Map();
rooms = new Map(); // roomId -> clientIds
events = new Map(); // roomId -> events
config;
constructor(config = {}) {
super();
this.config = {
port: 8080,
auth: {},
cors: { origin: '*', credentials: true },
rateLimit: { maxConnections: 1000, maxEventsPerSecond: 100 },
storage: { type: 'memory' },
debug: false,
...config
};
this.server = createServer();
this.wss = new WebSocketServer({
server: this.server,
verifyClient: this.verifyClient.bind(this)
});
this.setupServer();
}
// 🚀 Super simple API for users
async start() {
return new Promise((resolve) => {
this.server.listen(this.config.port, () => {
console.log(`🚀 kSync Server running on ws://localhost:${this.config.port}`);
resolve();
});
});
}
async stop() {
return new Promise((resolve) => {
this.wss.close();
this.server.close(() => resolve());
});
}
// 📡 Broadcast to all clients in a room
broadcast(room, data) {
const roomClients = this.rooms.get(room);
if (!roomClients)
return;
const message = {
type: 'event',
room,
data,
timestamp: Date.now()
};
roomClients.forEach(clientId => {
const client = this.clients.get(clientId);
if (client?.ws.readyState === WebSocket.OPEN) {
client.ws.send(JSON.stringify(message));
}
});
// Store event for sync
if (!this.events.has(room)) {
this.events.set(room, []);
}
const roomEvents = this.events.get(room);
if (roomEvents) {
roomEvents.push(data);
}
}
// 🎯 Send to specific client
sendToClient(clientId, data) {
const client = this.clients.get(clientId);
if (client?.ws.readyState === WebSocket.OPEN) {
client.ws.send(JSON.stringify({
type: 'event',
data,
timestamp: Date.now()
}));
}
}
// 👥 Get all clients in a room
getRoomClients(room) {
const roomClients = this.rooms.get(room);
if (!roomClients)
return [];
return Array.from(roomClients)
.map(id => this.clients.get(id))
.filter(Boolean);
}
// 📊 Get server stats
getStats() {
return {
clients: this.clients.size,
rooms: this.rooms.size,
events: Array.from(this.events.values()).reduce((sum, events) => sum + events.length, 0),
uptime: process.uptime()
};
}
// 🔧 Private setup methods
setupServer() {
this.wss.on('connection', this.handleConnection.bind(this));
// Periodic cleanup
setInterval(() => this.cleanup(), 60000);
// Ping clients
setInterval(() => this.pingClients(), 30000);
}
verifyClient(info) {
// Basic rate limiting
if (this.clients.size >= (this.config.rateLimit?.maxConnections || 1000)) {
return false;
}
// CORS check
const origin = info.origin;
const allowedOrigins = this.config.cors.origin;
if (allowedOrigins !== '*' && Array.isArray(allowedOrigins)) {
return allowedOrigins.includes(origin);
}
return true;
}
handleConnection(ws) {
const clientId = this.generateClientId();
const client = {
id: clientId,
ws,
rooms: new Set(),
lastPing: Date.now(),
metadata: {}
};
this.clients.set(clientId, client);
this.log(`Client connected: ${clientId}`);
ws.on('message', (data) => this.handleMessage(client, data));
ws.on('close', () => this.handleDisconnect(client));
ws.on('error', (error) => this.handleError(client, error));
ws.on('pong', () => client.lastPing = Date.now());
// Send welcome message
this.sendToClient(clientId, {
type: 'connected',
clientId,
serverTime: Date.now()
});
this.emit('client-connected', client);
}
async handleMessage(client, data) {
try {
const message = JSON.parse(data.toString());
switch (message.type) {
case 'auth':
await this.handleAuth(client, message);
break;
case 'join':
this.handleJoin(client, message);
break;
case 'leave':
this.handleLeave(client, message);
break;
case 'event':
this.handleEvent(client, message);
break;
case 'sync':
this.handleSync(client, message);
break;
case 'ping':
client.ws.send(JSON.stringify({ type: 'pong' }));
break;
}
}
catch (error) {
this.log(`Error handling message from ${client.id}:`, error);
}
}
async handleAuth(client, message) {
if (!this.config.auth.verify) {
client.userId = message.data?.userId || 'anonymous';
this.sendToClient(client.id, { type: 'auth-success', userId: client.userId });
return;
}
try {
const result = await this.config.auth.verify(message.data?.token);
client.userId = result.userId;
client.metadata = { ...client.metadata, ...result };
this.sendToClient(client.id, {
type: 'auth-success',
userId: client.userId,
metadata: client.metadata
});
this.emit('client-authenticated', client);
}
catch (error) {
this.sendToClient(client.id, {
type: 'auth-error',
error: 'Authentication failed'
});
}
}
handleJoin(client, message) {
const room = message.room;
if (!room)
return;
// Add client to room
client.rooms.add(room);
if (!this.rooms.has(room)) {
this.rooms.set(room, new Set());
}
this.rooms.get(room).add(client.id);
// Send existing events to sync client
const roomEvents = this.events.get(room) || [];
if (roomEvents.length > 0) {
this.sendToClient(client.id, {
type: 'sync',
room,
events: roomEvents
});
}
// Notify other clients (not the joining client themselves)
const roomClients = this.rooms.get(room);
if (roomClients) {
const joinMessage = {
type: 'user-joined',
clientId: client.id,
userId: client.userId,
timestamp: Date.now()
};
roomClients.forEach(clientId => {
if (clientId !== client.id) {
const targetClient = this.clients.get(clientId);
if (targetClient?.ws.readyState === WebSocket.OPEN) {
targetClient.ws.send(JSON.stringify(joinMessage));
}
}
});
}
this.log(`Client ${client.id} joined room: ${room}`);
this.emit('client-joined-room', client, room);
}
handleLeave(client, message) {
const room = message.room;
if (!room)
return;
this.removeClientFromRoom(client, room);
}
handleEvent(client, message) {
const room = message.room;
if (!room || !client.rooms.has(room))
return;
// Add metadata
const eventData = {
...message.data,
_meta: {
clientId: client.id,
userId: client.userId,
timestamp: Date.now()
}
};
// **FIX: Store event in server memory**
if (!this.events.has(room)) {
this.events.set(room, []);
}
this.events.get(room).push(eventData);
// Keep only last 1000 events per room to prevent memory bloat
const roomEvents = this.events.get(room);
if (roomEvents.length > 1000) {
roomEvents.splice(0, roomEvents.length - 1000);
}
// Broadcast to other clients in room (excluding sender to prevent duplication)
const roomClients = this.rooms.get(room);
if (!roomClients)
return;
const messageToSend = {
type: 'event',
room,
data: eventData,
timestamp: Date.now()
};
roomClients.forEach(clientId => {
// Skip the sender to prevent event duplication
if (clientId !== client.id) {
const targetClient = this.clients.get(clientId);
if (targetClient?.ws.readyState === WebSocket.OPEN) {
targetClient.ws.send(JSON.stringify(messageToSend));
}
}
});
this.emit('event', room, eventData, client);
}
handleSync(client, message) {
const room = message.room;
if (!room)
return;
const roomEvents = this.events.get(room) || [];
const fromTimestamp = message.data?.fromTimestamp || 0;
const eventsToSync = roomEvents.filter(event => (event._meta?.timestamp || 0) > fromTimestamp);
this.sendToClient(client.id, {
type: 'sync',
room,
events: eventsToSync,
timestamp: Date.now()
});
}
handleDisconnect(client) {
// Remove from all rooms
client.rooms.forEach(room => {
this.removeClientFromRoom(client, room);
});
this.clients.delete(client.id);
this.log(`Client disconnected: ${client.id}`);
this.emit('client-disconnected', client);
}
handleError(client, error) {
this.log(`Client error (${client.id}):`, error);
this.emit('client-error', client, error);
}
removeClientFromRoom(client, room) {
client.rooms.delete(room);
const roomClients = this.rooms.get(room);
if (roomClients) {
roomClients.delete(client.id);
// Notify others
this.broadcast(room, {
type: 'user-left',
clientId: client.id,
userId: client.userId
});
// Clean up empty rooms
if (roomClients.size === 0) {
this.rooms.delete(room);
}
}
this.emit('client-left-room', client, room);
}
pingClients() {
this.clients.forEach(client => {
if (client.ws.readyState === WebSocket.OPEN) {
client.ws.ping();
}
});
}
cleanup() {
const now = Date.now();
const timeout = 60000; // 1 minute
this.clients.forEach(client => {
if (now - client.lastPing > timeout) {
client.ws.terminate();
this.handleDisconnect(client);
}
});
}
generateClientId() {
return `client-${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
log(...args) {
if (this.config.debug) {
console.log('[kSync]', ...args);
}
}
}
// 🎯 Quick start helpers
export function createKSyncServer(config) {
return new KSyncServer(config);
}
// 🚀 One-liner server start
export async function startKSyncServer(port = 8080) {
const server = new KSyncServer({ port, debug: true });
await server.start();
return server;
}