@synet/realtime
Version:
Realtime Communication server/client implementations
139 lines (138 loc) • 5.27 kB
JavaScript
"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;