UNPKG

@synet/realtime

Version:

Realtime Communication server/client implementations

139 lines (138 loc) 5.27 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GunRealtimeServer = void 0; const gun_1 = __importDefault(require("gun")); const node_http_1 = __importDefault(require("node:http")); const node_path_1 = __importDefault(require("node:path")); const node_fs_1 = __importDefault(require("node:fs")); class GunRealtimeServer { constructor(options = {}, logger) { this.options = options; this.logger = logger; this.clients = new Map(); this.subscriptions = new Map(); // topic -> client ids this.startTime = Date.now(); this.eventHandlers = new Map(); this.topicSubscriptions = new Map(); this.stats = { connectedClients: 0, totalTopics: 0, messagesSent: 0, messagesReceived: 0, uptime: 0 }; } async start() { const port = this.options.transportOptions?.port || 8765; const storagePath = this.options.transportOptions?.storagePath || 'storage/gun'; const dataDir = node_path_1.default.join(storagePath); // Ensure storage directory exists if (!node_fs_1.default.existsSync(dataDir)) { node_fs_1.default.mkdirSync(dataDir, { recursive: true }); } // Create HTTP server this.httpServer = node_http_1.default.createServer(); // Initialize GUN with our HTTP server this.gun = (0, gun_1.default)({ web: this.httpServer, file: node_path_1.default.join(dataDir, "radata"), multicast: this.options.transportOptions?.multicast !== false, peers: this.options.transportOptions?.peers || [], axe: this.options.transportOptions?.axe !== false }); // Start HTTP server this.httpServer.listen(port, () => { this.logger?.info(`🔫 GUN RealtimeServer running at http://localhost:${port}/gun`); this.logger?.info(`Data stored in: ${node_path_1.default.join(dataDir, "radata")}`); }); } async stop() { // Clear event checking interval if (this.checkIntervalId) { clearInterval(this.checkIntervalId); } // Unsubscribe from all topics for (const [topic, sub] of this.topicSubscriptions.entries()) { try { // Gun doesn't have a clean unsubscribe mechanism, // but we can off() specific callbacks if (this.gun && sub.callback) { this.gun.get(topic).get(sub.eventType).off(sub.callback); } } catch (error) { this.logger?.error(`Error unsubscribing from topic ${topic}:`, error); } } // Clear maps this.topicSubscriptions.clear(); this.subscriptions.clear(); this.clients.clear(); // Close HTTP server return new Promise((resolve) => { if (this.httpServer) { this.httpServer.close(() => { this.logger?.info('🔫 GUN RealtimeServer stopped'); resolve(); }); } else { resolve(); } }); } async broadcast(topic, event) { if (!this.gun) { throw new Error("GUN server not initialized"); } try { // Store the event in GUN under the topic structure this.gun.get(topic).get(event.type).put({ id: event.id, source: event.source, type: event.type, timestamp: event.timestamp.toISOString(), data: event.data, // Store directly, not as a reference metadata: event.metadata }); // Also store as simple direct data to avoid reference issues this.gun.get(topic).get(event.type).put({ id: event.id, source: event.source, type: event.type, timestamp: event.timestamp.toISOString(), data: event.data, // Store directly, not as a reference metadata: event.metadata }); // Update stats this.stats.messagesSent++; this.logger?.debug(`Broadcasted to ${topic}: Event ${event.type} (${event.id})`); return Promise.resolve(); } catch (error) { this.logger?.error(`Error broadcasting to ${topic}:`, error); return Promise.reject(error); } } getStats() { return { ...this.stats, connectedClients: this.clients.size, totalTopics: this.subscriptions.size, uptime: Date.now() - this.startTime }; } on(event, handler) { if (!this.eventHandlers.has(event)) { this.eventHandlers.set(event, new Set()); } const handlers = this.eventHandlers.get(event); if (handlers) { handlers.add(handler); } } } exports.GunRealtimeServer = GunRealtimeServer;