anon-identity
Version:
Decentralized identity framework with DIDs, Verifiable Credentials, and privacy-preserving selective disclosure
340 lines • 12.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ActivityWebSocketServer = exports.MessageType = void 0;
const ws_1 = require("ws");
var MessageType;
(function (MessageType) {
// Client to server
MessageType["SUBSCRIBE"] = "subscribe";
MessageType["UNSUBSCRIBE"] = "unsubscribe";
MessageType["GET_RECENT_EVENTS"] = "get_recent_events";
MessageType["GET_METRICS"] = "get_metrics";
MessageType["PING"] = "ping";
// Server to client
MessageType["EVENT"] = "event";
MessageType["SUBSCRIBED"] = "subscribed";
MessageType["UNSUBSCRIBED"] = "unsubscribed";
MessageType["RECENT_EVENTS"] = "recent_events";
MessageType["METRICS"] = "metrics";
MessageType["PONG"] = "pong";
MessageType["ERROR"] = "error";
})(MessageType || (exports.MessageType = MessageType = {}));
class ActivityWebSocketServer {
constructor(streamManager, server, config = {}) {
this.clients = new Map();
this.clientSubscriptions = new Map();
this.streamManager = streamManager;
this.config = {
port: config.port || 8080,
path: config.path || '/activity-stream',
enableAuth: config.enableAuth ?? false,
authTimeout: config.authTimeout || 30000,
maxConnections: config.maxConnections || 1000,
pingInterval: config.pingInterval || 30000,
enableCompression: config.enableCompression ?? true
};
// Create WebSocket server
const wsOptions = {
path: this.config.path,
perMessageDeflate: this.config.enableCompression
};
if (server) {
wsOptions.server = server;
}
else {
wsOptions.port = this.config.port;
}
this.wss = new ws_1.WebSocketServer(wsOptions);
this.setupEventHandlers();
this.startPingTimer();
}
/**
* Get server statistics
*/
getStats() {
const subscriptionsByType = {};
let totalSubscriptions = 0;
this.clientSubscriptions.forEach(subs => {
totalSubscriptions += subs.length;
subs.forEach(sub => {
const filterType = this.getFilterType(sub.filters);
subscriptionsByType[filterType] = (subscriptionsByType[filterType] || 0) + 1;
});
});
return {
connectedClients: this.clients.size,
totalSubscriptions,
subscriptionsByType,
uptime: process.uptime()
};
}
/**
* Broadcast event to all matching clients
*/
broadcast(event, filters) {
const message = {
type: MessageType.EVENT,
data: event
};
this.clients.forEach((ws, clientId) => {
if (filters) {
// Check if client has matching subscriptions
const clientSubs = this.clientSubscriptions.get(clientId) || [];
const hasMatchingSubscription = clientSubs.some(sub => this.filtersMatch(sub.filters, filters));
if (!hasMatchingSubscription) {
return;
}
}
this.sendMessage(ws, message);
});
}
/**
* Send event to specific client
*/
sendToClient(clientId, event) {
const ws = this.clients.get(clientId);
if (ws && ws.readyState === ws_1.WebSocket.OPEN) {
const message = {
type: MessageType.EVENT,
data: event
};
this.sendMessage(ws, message);
}
}
/**
* Close the WebSocket server
*/
close() {
return new Promise((resolve) => {
if (this.pingTimer) {
clearInterval(this.pingTimer);
}
// Close all client connections
this.clients.forEach(ws => {
ws.close(1000, 'Server shutting down');
});
// Close server
this.wss.close(() => {
resolve();
});
});
}
// Private methods
setupEventHandlers() {
this.wss.on('connection', (ws, request) => {
const clientId = this.generateClientId();
// Check connection limit
if (this.clients.size >= this.config.maxConnections) {
ws.close(1008, 'Maximum connections exceeded');
return;
}
this.clients.set(clientId, ws);
this.clientSubscriptions.set(clientId, []);
console.log(`Client connected: ${clientId} (${this.clients.size} total)`);
// Set up client event handlers
ws.on('message', (data) => {
this.handleClientMessage(clientId, ws, data);
});
ws.on('close', (code, reason) => {
this.handleClientDisconnect(clientId, code, reason.toString());
});
ws.on('error', (error) => {
console.error(`WebSocket error for client ${clientId}:`, error);
this.handleClientDisconnect(clientId, 1011, 'Unexpected error');
});
ws.on('pong', () => {
// Client responded to ping - connection is alive
ws.isAlive = true;
});
// Send welcome message
this.sendMessage(ws, {
type: MessageType.PONG,
data: {
clientId,
message: 'Connected to activity stream',
serverTime: new Date().toISOString()
}
});
});
this.wss.on('error', (error) => {
console.error('WebSocket server error:', error);
});
}
handleClientMessage(clientId, ws, data) {
try {
const message = JSON.parse(data.toString());
switch (message.type) {
case MessageType.SUBSCRIBE:
this.handleSubscribe(clientId, ws, message);
break;
case MessageType.UNSUBSCRIBE:
this.handleUnsubscribe(clientId, ws, message);
break;
case MessageType.GET_RECENT_EVENTS:
this.handleGetRecentEvents(clientId, ws, message);
break;
case MessageType.GET_METRICS:
this.handleGetMetrics(clientId, ws, message);
break;
case MessageType.PING:
this.sendMessage(ws, { type: MessageType.PONG, id: message.id });
break;
default:
this.sendError(ws, `Unknown message type: ${message.type}`, message.id);
}
}
catch (error) {
this.sendError(ws, `Invalid message format: ${error}`);
}
}
handleSubscribe(clientId, ws, message) {
try {
const filters = message.data?.filters || {};
// Create stream subscription
const streamSubscription = this.streamManager.subscribe(filters, (event) => {
this.sendToClient(clientId, event);
}, { clientId, subscriptionId: message.id });
// Store client subscription
const clientSubscription = {
subscriptionId: message.id || this.generateId(),
streamSubscription,
filters,
clientId
};
const clientSubs = this.clientSubscriptions.get(clientId) || [];
clientSubs.push(clientSubscription);
this.clientSubscriptions.set(clientId, clientSubs);
this.sendMessage(ws, {
type: MessageType.SUBSCRIBED,
id: message.id,
data: {
subscriptionId: clientSubscription.subscriptionId,
filters
}
});
}
catch (error) {
this.sendError(ws, `Subscription failed: ${error}`, message.id);
}
}
handleUnsubscribe(clientId, ws, message) {
const subscriptionId = message.data?.subscriptionId;
if (!subscriptionId) {
this.sendError(ws, 'Subscription ID required', message.id);
return;
}
const clientSubs = this.clientSubscriptions.get(clientId) || [];
const subscriptionIndex = clientSubs.findIndex(sub => sub.subscriptionId === subscriptionId);
if (subscriptionIndex === -1) {
this.sendError(ws, 'Subscription not found', message.id);
return;
}
const subscription = clientSubs[subscriptionIndex];
subscription.streamSubscription.unsubscribe();
clientSubs.splice(subscriptionIndex, 1);
this.sendMessage(ws, {
type: MessageType.UNSUBSCRIBED,
id: message.id,
data: { subscriptionId }
});
}
handleGetRecentEvents(clientId, ws, message) {
const { filters, limit } = message.data || {};
const events = this.streamManager.getRecentEvents(filters, limit);
this.sendMessage(ws, {
type: MessageType.RECENT_EVENTS,
id: message.id,
data: { events, count: events.length }
});
}
handleGetMetrics(clientId, ws, message) {
const streamMetrics = this.streamManager.getMetrics();
const subscriptionStats = this.streamManager.getSubscriptionStats();
const serverStats = this.getStats();
this.sendMessage(ws, {
type: MessageType.METRICS,
id: message.id,
data: {
stream: streamMetrics,
subscriptions: subscriptionStats,
server: serverStats
}
});
}
handleClientDisconnect(clientId, code, reason) {
console.log(`Client disconnected: ${clientId} (${code}: ${reason})`);
// Clean up subscriptions
const clientSubs = this.clientSubscriptions.get(clientId) || [];
clientSubs.forEach(sub => {
sub.streamSubscription.unsubscribe();
});
this.clientSubscriptions.delete(clientId);
this.clients.delete(clientId);
}
sendMessage(ws, message) {
if (ws.readyState === ws_1.WebSocket.OPEN) {
try {
ws.send(JSON.stringify(message));
}
catch (error) {
console.error('Failed to send message:', error);
}
}
}
sendError(ws, error, messageId) {
this.sendMessage(ws, {
type: MessageType.ERROR,
id: messageId,
error
});
}
startPingTimer() {
this.pingTimer = setInterval(() => {
this.wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
ws.terminate();
return;
}
ws.isAlive = false;
ws.ping();
});
}, this.config.pingInterval);
}
generateClientId() {
return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
generateId() {
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
getFilterType(filters) {
if (filters.agentDID)
return 'agent';
if (filters.parentDID)
return 'user';
if (filters.critical)
return 'critical';
if (filters.errorOnly)
return 'errors';
if (filters.serviceDID)
return 'service';
if (filters.types)
return 'type-specific';
return 'general';
}
filtersMatch(filter1, filter2) {
// Simple filter matching - could be more sophisticated
if (filter2.agentDID && filter1.agentDID !== filter2.agentDID)
return false;
if (filter2.parentDID && filter1.parentDID !== filter2.parentDID)
return false;
if (filter2.serviceDID && filter1.serviceDID !== filter2.serviceDID)
return false;
if (filter2.critical && !filter1.critical)
return false;
if (filter2.errorOnly && !filter1.errorOnly)
return false;
return true;
}
}
exports.ActivityWebSocketServer = ActivityWebSocketServer;
//# sourceMappingURL=websocket-server.js.map