Files
boc/quixzoom-capture-pipeline/websocket/realtime-server.js
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

401 lines
10 KiB
JavaScript

/**
* QUIXZOOM Real-Time WebSocket Server
*
* Hanterar realtidskommunikation mellan Zoomers och backend.
*
* Flöde:
* Zoomer → Observation → WebSocket → Identity Engine → Knowledge Graph → Mission Generator → AI → Instruktion tillbaka
*
* Features:
* - Låg fördröjning (< 100ms)
* - Binära meddelanden för video/bilder
* - JSON för metadata och instruktioner
* - Heartbeat för att upptäcka frånkoppling
* - Rum (rooms) för geografiska områden
* - Autentisering via JWT
*/
const WebSocket = require('ws');
const http = require('http');
const crypto = require('crypto');
class RealtimeServer {
constructor(options = {}) {
this.port = options.port || 8080;
this.heartbeatInterval = options.heartbeatInterval || 30000;
this.maxMessageSize = options.maxMessageSize || 10 * 1024 * 1024; // 10MB
this.wss = null;
this.clients = new Map(); // clientId -> { ws, zoomerId, location, room }
this.rooms = new Map(); // roomId -> Set(clientIds)
this.messageHandlers = new Map();
// Statistik
this.stats = {
connections: 0,
messagesReceived: 0,
messagesSent: 0,
bytesReceived: 0,
bytesSent: 0,
errors: 0,
};
}
/**
* ============================================================
* STARTA SERVER
* ============================================================
*/
start() {
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'ok',
service: 'quixzoom-websocket',
stats: this.stats,
connections: this.clients.size,
}));
});
this.wss = new WebSocket.Server({
server,
maxPayload: this.maxMessageSize,
});
this.wss.on('connection', (ws, req) => this.handleConnection(ws, req));
server.listen(this.port, () => {
console.log(`[WS] Real-time server running on port ${this.port}`);
});
// Starta heartbeat
this.startHeartbeat();
return server;
}
/**
* ============================================================
* HANDLERS
* ============================================================
*/
registerHandler(messageType, handler) {
this.messageHandlers.set(messageType, handler);
console.log(`[WS] Registered handler: ${messageType}`);
}
/**
* ============================================================
* CONNECTION HANDLING
* ============================================================
*/
handleConnection(ws, req) {
const clientId = this.generateClientId();
const client = {
id: clientId,
ws,
zoomerId: null,
location: null,
room: null,
connectedAt: Date.now(),
lastPing: Date.now(),
messagesReceived: 0,
messagesSent: 0,
};
this.clients.set(clientId, client);
this.stats.connections++;
console.log(`[WS] Client connected: ${clientId} (total: ${this.clients.size})`);
// Skicka välkomstmeddelande
this.sendToClient(clientId, {
type: 'connected',
clientId,
timestamp: Date.now(),
});
ws.on('message', (data) => this.handleMessage(clientId, data));
ws.on('close', () => this.handleDisconnect(clientId));
ws.on('error', (error) => this.handleError(clientId, error));
ws.on('pong', () => {
client.lastPing = Date.now();
});
}
handleMessage(clientId, data) {
const client = this.clients.get(clientId);
if (!client) return;
this.stats.messagesReceived++;
this.stats.bytesReceived += data.length;
client.messagesReceived++;
try {
const message = JSON.parse(data);
console.log(`[WS] [${clientId}] ${message.type}`);
// Autentisering
if (message.type === 'auth') {
this.handleAuth(clientId, message.payload);
return;
}
// Kräv autentisering
if (!client.zoomerId) {
this.sendToClient(clientId, {
type: 'error',
error: 'Not authenticated',
});
return;
}
// Hantera meddelande
const handler = this.messageHandlers.get(message.type);
if (handler) {
handler(message.payload, client, this);
} else {
console.log(`[WS] No handler for: ${message.type}`);
}
} catch (error) {
console.error(`[WS] Error parsing message:`, error.message);
this.stats.errors++;
}
}
handleAuth(clientId, payload) {
const client = this.clients.get(clientId);
if (!client) return;
// TODO: Verifiera JWT
const zoomerId = payload.zoomerId || `zoomer_${clientId}`;
client.zoomerId = zoomerId;
console.log(`[WS] Client authenticated: ${clientId} as ${zoomerId}`);
this.sendToClient(clientId, {
type: 'auth_success',
zoomerId,
});
}
handleDisconnect(clientId) {
const client = this.clients.get(clientId);
if (!client) return;
// Ta bort från rum
if (client.room) {
this.leaveRoom(clientId, client.room);
}
this.clients.delete(clientId);
console.log(`[WS] Client disconnected: ${clientId} (total: ${this.clients.size})`);
}
handleError(clientId, error) {
console.error(`[WS] Client error: ${clientId}`, error.message);
this.stats.errors++;
}
/**
* ============================================================
* RUM (GEOGRAFISKA OMRÅDEN)
* ============================================================
*/
joinRoom(clientId, roomId) {
const client = this.clients.get(clientId);
if (!client) return;
// Lämna tidigare rum
if (client.room) {
this.leaveRoom(clientId, client.room);
}
// Gå med i nytt rum
if (!this.rooms.has(roomId)) {
this.rooms.set(roomId, new Set());
}
this.rooms.get(roomId).add(clientId);
client.room = roomId;
console.log(`[WS] ${clientId} joined room: ${roomId}`);
}
leaveRoom(clientId, roomId) {
const room = this.rooms.get(roomId);
if (room) {
room.delete(clientId);
if (room.size === 0) {
this.rooms.delete(roomId);
}
}
const client = this.clients.get(clientId);
if (client) {
client.room = null;
}
console.log(`[WS] ${clientId} left room: ${roomId}`);
}
broadcastToRoom(roomId, message, excludeClientId = null) {
const room = this.rooms.get(roomId);
if (!room) return;
for (const clientId of room) {
if (clientId !== excludeClientId) {
this.sendToClient(clientId, message);
}
}
}
/**
* ============================================================
* SKICKA MEDDELANDEN
* ============================================================
*/
sendToClient(clientId, message) {
const client = this.clients.get(clientId);
if (!client || client.ws.readyState !== WebSocket.OPEN) return;
const data = JSON.stringify(message);
client.ws.send(data);
client.messagesSent++;
this.stats.messagesSent++;
this.stats.bytesSent += data.length;
}
broadcast(message, excludeClientId = null) {
for (const [clientId, client] of this.clients) {
if (clientId !== excludeClientId && client.ws.readyState === WebSocket.OPEN) {
const data = JSON.stringify(message);
client.ws.send(data);
client.messagesSent++;
this.stats.messagesSent++;
this.stats.bytesSent += data.length;
}
}
}
/**
* ============================================================
* HEARTBEAT
* ============================================================
*/
startHeartbeat() {
setInterval(() => {
const now = Date.now();
for (const [clientId, client] of this.clients) {
if (now - client.lastPing > this.heartbeatInterval * 2) {
console.log(`[WS] Client timeout: ${clientId}`);
client.ws.terminate();
this.handleDisconnect(clientId);
} else {
client.ws.ping();
}
}
}, this.heartbeatInterval);
}
/**
* ============================================================
* HJÄLPMETODER
* ============================================================
*/
generateClientId() {
return crypto.randomBytes(8).toString('hex');
}
getStats() {
return {
...this.stats,
activeConnections: this.clients.size,
activeRooms: this.rooms.size,
};
}
}
// Exportera
module.exports = RealtimeServer;
// Demo
if (require.main === module) {
const server = new RealtimeServer({ port: 8080 });
// Registrera handlers
server.registerHandler('observation', async (payload, client, server) => {
console.log(`[WS] Observation from ${client.zoomerId}:`, payload.objectType);
// Simulera processing
await new Promise(resolve => setTimeout(resolve, 50));
// Skicka bekräftelse
server.sendToClient(client.id, {
type: 'observation_ack',
observationId: payload.observationId,
status: 'received',
});
// Simulera Identity Engine
await new Promise(resolve => setTimeout(resolve, 100));
server.sendToClient(client.id, {
type: 'identity_result',
observationId: payload.observationId,
objectId: `street_lamp_${Math.floor(Math.random() * 50000).toString().padStart(6, '0')}`,
confidence: 0.85 + Math.random() * 0.15,
});
// Simulera Mission Generator
await new Promise(resolve => setTimeout(resolve, 100));
if (Math.random() > 0.7) {
server.sendToClient(client.id, {
type: 'instruction',
instruction: 'Gå två meter närmare för bättre vinkel',
priority: 'medium',
});
}
});
server.registerHandler('location_update', (payload, client, server) => {
client.location = payload.location;
// Uppdatera rum baserat på plats
const roomId = server.getRoomForLocation(payload.location);
if (roomId !== client.room) {
server.joinRoom(client.id, roomId);
}
});
server.registerHandler('telemetry', (payload, client, server) => {
// Spara telemetri
// console.log(`[WS] Telemetry from ${client.zoomerId}:`, payload);
});
// Hjälpmetod för rum
server.getRoomForLocation = (location) => {
const lat = Math.floor(location.lat * 10);
const lng = Math.floor(location.lng * 10);
return `${lat},${lng}`;
};
server.start();
console.log('\n=== WEBSOCKET SERVER DEMO ===');
console.log('Server running on ws://localhost:8080');
console.log('');
console.log('Test with:');
console.log(' wscat -c ws://localhost:8080');
console.log('');
console.log('Or use the client below...');
}