UNPKG

mudvault-mesh

Version:

Node.js client library for connecting to MudVault Mesh network

284 lines 10.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MeshClient = void 0; const ws_1 = __importDefault(require("ws")); const events_1 = require("events"); const utils_1 = require("./utils"); class MeshClient extends events_1.EventEmitter { constructor(options) { super(); this.ws = null; this.state = { connected: false, authenticated: false, reconnectAttempts: 0 }; this.heartbeatTimer = null; this.reconnectTimer = null; this.gatewayUrl = ''; this.options = { mudName: options.mudName, autoReconnect: options.autoReconnect ?? true, reconnectInterval: options.reconnectInterval ?? 5000, maxReconnectAttempts: options.maxReconnectAttempts ?? 10, heartbeatInterval: options.heartbeatInterval ?? 30000, timeout: options.timeout ?? 10000 }; if (!this.options.mudName || typeof this.options.mudName !== 'string') { throw new Error('MUD name is required and must be a string'); } } async connect(gatewayUrl = 'wss://mesh.mudvault.org', apiKey) { if (this.state.connected) { throw new Error('Already connected'); } this.gatewayUrl = gatewayUrl; return new Promise((resolve, reject) => { try { this.ws = new ws_1.default(gatewayUrl); const connectTimeout = setTimeout(() => { this.ws?.close(); reject(new Error('Connection timeout')); }, this.options.timeout); this.ws.on('open', () => { clearTimeout(connectTimeout); this.state.connected = true; this.state.reconnectAttempts = 0; this.emit('connected'); this.authenticate(apiKey).then(() => { resolve(); }).catch(reject); }); this.ws.on('message', (data) => { this.handleMessage(data); }); this.ws.on('close', (code, reason) => { this.handleDisconnection(code, reason.toString()); }); this.ws.on('error', (error) => { clearTimeout(connectTimeout); this.emit('error', error); if (!this.state.connected) { reject(error); } }); this.ws.on('pong', () => { this.state.lastPong = Date.now(); }); } catch (error) { reject(error); } }); } disconnect() { this.options.autoReconnect = false; if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.ws) { this.ws.close(); this.ws = null; } this.state.connected = false; this.state.authenticated = false; } async authenticate(apiKey) { const authMessage = (0, utils_1.createMessage)('auth', { mud: this.options.mudName }, { mud: 'Gateway' }, { mudName: this.options.mudName, token: apiKey }); this.sendMessage(authMessage); this.startHeartbeat(); // Authentication success is implied if no error is received setTimeout(() => { this.state.authenticated = true; this.emit('authenticated'); }, 1000); } handleMessage(data) { try { const messageText = data.toString(); const messageData = JSON.parse(messageText); // Basic validation if (!messageData.type || !messageData.from || !messageData.to) { this.emit('error', new Error('Invalid message received: missing required fields')); return; } const message = messageData; // Handle special message types switch (message.type) { case 'ping': this.handlePing(message); break; case 'pong': this.handlePong(message); break; case 'error': this.emit('error', new Error(`Server error: ${message.payload.message}`)); break; default: this.emit('message', message); this.emit(message.type, message); } } catch (error) { this.emit('error', new Error(`Failed to parse message: ${error.message}`)); } } handlePing(message) { const pongMessage = (0, utils_1.createPongMessage)({ mud: this.options.mudName }, { mud: 'Gateway' }, message.payload.timestamp); this.sendMessage(pongMessage); } handlePong(message) { this.state.lastPong = Date.now(); const latency = this.state.lastPong - message.payload.timestamp; this.emit('pong', { latency }); } handleDisconnection(code, reason) { this.state.connected = false; this.state.authenticated = false; if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } this.emit('disconnected', { code, reason }); if (this.options.autoReconnect && this.state.reconnectAttempts < this.options.maxReconnectAttempts) { this.scheduleReconnect(); } } scheduleReconnect() { if (this.reconnectTimer) { return; } const delay = this.options.reconnectInterval * Math.pow(2, this.state.reconnectAttempts); this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null; this.state.reconnectAttempts++; this.emit('reconnecting', { attempt: this.state.reconnectAttempts }); this.connect(this.gatewayUrl).catch((error) => { this.emit('reconnectFailed', { attempt: this.state.reconnectAttempts, error }); if (this.state.reconnectAttempts < this.options.maxReconnectAttempts) { this.scheduleReconnect(); } else { this.emit('reconnectGiveUp'); } }); }, delay); } startHeartbeat() { if (this.heartbeatTimer) { return; } this.heartbeatTimer = setInterval(() => { if (!this.state.connected || !this.ws) { return; } const now = Date.now(); // Check if we've missed too many pongs if (this.state.lastPong && (now - this.state.lastPong) > (this.options.heartbeatInterval * 2)) { this.ws.close(); return; } // Send ping this.state.lastPing = now; const pingMessage = (0, utils_1.createPingMessage)({ mud: this.options.mudName }, { mud: 'Gateway' }); this.sendMessage(pingMessage); }, this.options.heartbeatInterval); } sendMessage(message) { if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) { throw new Error('Not connected to gateway'); } this.ws.send(JSON.stringify(message)); } // Public API methods sendTell(to, message) { const tellMessage = (0, utils_1.createTellMessage)({ mud: this.options.mudName, user: 'System' }, to, message); this.sendMessage(tellMessage); } sendChannelMessage(channel, message, user) { const channelMessage = (0, utils_1.createChannelMessage)({ mud: this.options.mudName, user: user || 'System' }, channel, message); this.sendMessage(channelMessage); } joinChannel(channel, user) { const joinMessage = (0, utils_1.createChannelMessage)({ mud: this.options.mudName, user: user || 'System' }, channel, '', 'join'); this.sendMessage(joinMessage); } leaveChannel(channel, user) { const leaveMessage = (0, utils_1.createChannelMessage)({ mud: this.options.mudName, user: user || 'System' }, channel, '', 'leave'); this.sendMessage(leaveMessage); } requestWho(targetMud) { const whoMessage = (0, utils_1.createWhoRequestMessage)({ mud: this.options.mudName }, targetMud); this.sendMessage(whoMessage); } requestFinger(targetMud, targetUser) { const fingerMessage = (0, utils_1.createFingerRequestMessage)({ mud: this.options.mudName }, targetMud, targetUser); this.sendMessage(fingerMessage); } requestLocate(targetUser) { const locateMessage = (0, utils_1.createLocateRequestMessage)({ mud: this.options.mudName }, targetUser); this.sendMessage(locateMessage); } setUserOnline(userInfo) { const presenceMessage = (0, utils_1.createMessage)('presence', { mud: this.options.mudName, user: userInfo.username }, { mud: 'Gateway' }, { status: 'online', activity: userInfo.location, location: userInfo.location }); this.sendMessage(presenceMessage); } setUserOffline(username) { const presenceMessage = (0, utils_1.createMessage)('presence', { mud: this.options.mudName, user: username }, { mud: 'Gateway' }, { status: 'offline' }); this.sendMessage(presenceMessage); } // Getters isConnected() { return this.state.connected; } isAuthenticated() { return this.state.authenticated; } getConnectionState() { return { ...this.state }; } getMudName() { return this.options.mudName; } // Event handler convenience methods onTell(handler) { return this.on('tell', handler); } onChannel(handler) { return this.on('channel', handler); } onWho(handler) { return this.on('who', handler); } onFinger(handler) { return this.on('finger', handler); } onLocate(handler) { return this.on('locate', handler); } onEmote(handler) { return this.on('emote', handler); } onEmoteTo(handler) { return this.on('emoteto', handler); } } exports.MeshClient = MeshClient; //# sourceMappingURL=client.js.map