mudvault-mesh
Version:
Node.js client library for connecting to MudVault Mesh network
284 lines • 10.8 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.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