@eventmsg/core
Version:
EventMsgV3 TypeScript library - Core protocol implementation with transport abstraction
552 lines (550 loc) • 17.3 kB
JavaScript
import { ValidationError } from "./errors/protocol-error.js";
import { WaitForTimeoutError } from "./errors/timeout-error.js";
import { ConnectionError, DisconnectionError, SendError, TransportError } from "./errors/transport-error.js";
import { AddressValidationError } from "./errors/validation-error.js";
import { configureLogging, getLogger, hexDump } from "./internal/logger.js";
import { DEFAULT_CONFIG } from "./types/config.js";
import { Protocol } from "./protocol.js";
import { INTERNAL_EVENTS } from "./types/events.js";
import { EventEmitter } from "eventemitter3";
//#region src/event-msg.ts
/**
* EventMsg - Main class for EventMsgV3 protocol communication
*
* Provides type-safe messaging with per-method generics, async event handling,
* and transport abstraction.
*/
var EventMsg = class extends EventEmitter {
transport;
protocol;
logger;
messageCounter = 0;
config;
connectionState = false;
connectTime;
pendingWaits = /* @__PURE__ */ new Map();
stats;
messageHandlers = /* @__PURE__ */ new Map();
constructor(config) {
super();
this.config = {
...DEFAULT_CONFIG,
...config
};
this.transport = config.transport;
configureLogging(this.config.logging);
this.protocol = new Protocol({
...this.config,
transport: this.transport
});
this.logger = getLogger("CORE");
this.stats = {
messagesSent: 0,
messagesReceived: 0,
uptime: 0,
connected: false
};
this.setupTransportHandlers();
}
/**
* Connect to the transport layer
*/
async connect() {
try {
if (this.connectionState) return;
await this.transport.connect();
this.connectionState = true;
this.connectTime = /* @__PURE__ */ new Date();
this.stats.connected = true;
this.emit(INTERNAL_EVENTS.CONNECT);
this.logger.success("Connected to transport", {
localAddress: this.transport.getLocalAddress(),
groupAddress: this.transport.getGroupAddress(),
uptime: 0
});
} catch (error) {
this.logger.error("Failed to connect to transport", {
error: error instanceof Error ? error.message : String(error),
localAddress: this.transport.getLocalAddress(),
groupAddress: this.transport.getGroupAddress(),
cause: error instanceof Error ? error.name : "Unknown"
});
const transportError = new ConnectionError("Failed to connect to transport", {
context: {
localAddress: this.transport.getLocalAddress(),
groupAddress: this.transport.getGroupAddress()
},
cause: error instanceof Error ? error : new Error(String(error))
});
this.emit(INTERNAL_EVENTS.ERROR, transportError);
throw transportError;
}
}
/**
* Disconnect from the transport layer
*/
async disconnect() {
try {
if (!this.connectionState) return;
this.clearAllPendingWaits();
this.offMessage();
await this.transport.disconnect();
this.connectionState = false;
this.stats.connected = false;
this.emit(INTERNAL_EVENTS.DISCONNECT);
this.logger.info("Disconnected from transport", {
uptime: this.connectTime ? Date.now() - this.connectTime.getTime() : 0,
messagesSent: this.stats.messagesSent,
messagesReceived: this.stats.messagesReceived
});
} catch (error) {
this.logger.error("Failed to disconnect from transport", {
error: error instanceof Error ? error.message : String(error),
cause: error instanceof Error ? error.name : "Unknown",
uptime: this.connectTime ? Date.now() - this.connectTime.getTime() : 0
});
const transportError = new DisconnectionError("Failed to disconnect from transport", { cause: error instanceof Error ? error : new Error(String(error)) });
this.emit(INTERNAL_EVENTS.ERROR, transportError);
throw transportError;
}
}
/**
* Check if currently connected
*/
isConnected() {
return this.connectionState && this.transport.isConnected();
}
/**
* Send a message with explicit receiver targeting
*/
async send(event, data, options) {
try {
if (!this.isConnected()) throw new ConnectionError("Not connected to transport");
this.validateSendOptions(options);
const header = this.createHeader(options);
const eventData = data;
const encodedMessage = this.protocol.encode(header, event, eventData);
this.logger.debug("Message encoded", {
event,
header,
dataLength: eventData.length,
encodedSize: encodedMessage.length
});
this.logger.trace("Encoded message hex dump", { hex: hexDump(encodedMessage) });
await this.transport.send(encodedMessage);
this.stats.messagesSent++;
this.logger.info("Message sent", {
event,
receiverId: options.receiverId,
receiverGroupId: options.receiverGroupId,
messageId: header.messageId,
size: encodedMessage.length,
totalSent: this.stats.messagesSent
});
this.emit(INTERNAL_EVENTS.SEND, {
event,
data,
options,
header,
messageSize: encodedMessage.length
});
} catch (error) {
this.logger.error("Failed to send message", {
error: error instanceof Error ? error.message : String(error),
event,
receiverId: options.receiverId,
receiverGroupId: options.receiverGroupId,
dataLength: JSON.stringify(data).length,
cause: error instanceof Error ? error.name : "Unknown"
});
const sendError = new SendError("Failed to send message", {
context: {
event,
receiverId: options.receiverId,
receiverGroupId: options.receiverGroupId,
dataLength: JSON.stringify(data).length
},
cause: error instanceof Error ? error : new Error(String(error))
});
this.emit(INTERNAL_EVENTS.ERROR, sendError);
throw sendError;
}
}
/**
* Send binary data with explicit receiver targeting
*/
async sendBinary(event, data, options) {
try {
if (!this.isConnected()) throw new ConnectionError("Not connected to transport");
this.validateSendOptions(options);
const header = this.createHeader(options);
const encodedMessage = this.protocol.encodeBinary(header, event, data);
this.logger.debug("Binary message encoded", {
event,
header,
dataLength: data.length,
encodedSize: encodedMessage.length
});
this.logger.trace("Encoded binary message hex dump", { hex: hexDump(encodedMessage) });
await this.transport.send(encodedMessage);
this.stats.messagesSent++;
this.logger.info("Binary message sent", {
event,
receiverId: options.receiverId,
receiverGroupId: options.receiverGroupId,
messageId: header.messageId,
size: encodedMessage.length,
totalSent: this.stats.messagesSent
});
this.emit(INTERNAL_EVENTS.SEND, {
event,
data,
options,
header,
messageSize: encodedMessage.length
});
} catch (error) {
this.logger.error("Failed to send binary message", {
error: error instanceof Error ? error.message : String(error),
event,
receiverId: options.receiverId,
receiverGroupId: options.receiverGroupId,
dataLength: data.length,
cause: error instanceof Error ? error.name : "Unknown"
});
const sendError = new SendError("Failed to send binary message", {
context: {
event,
receiverId: options.receiverId,
receiverGroupId: options.receiverGroupId,
dataLength: data.length
},
cause: error instanceof Error ? error : new Error(String(error))
});
this.emit(INTERNAL_EVENTS.ERROR, sendError);
throw sendError;
}
}
/**
* Register a type-safe event handler
*/
on(event, handler) {
return super.on(event, handler);
}
/**
* Register a one-time type-safe event handler
*/
once(event, handler) {
return super.once(event, handler);
}
/**
* Remove a type-safe event handler
*/
off(event, handler) {
if (handler) return super.off(event, handler);
return super.removeAllListeners(event);
}
onMessage(eventNameOrHandler, handler) {
if (typeof eventNameOrHandler === "string") {
const targetEventName = eventNameOrHandler;
const specificHandler = handler;
const internalHandler$1 = (messageInfo) => {
if (messageInfo.eventName === targetEventName && specificHandler) specificHandler(messageInfo.data, messageInfo.metadata);
};
this.on(INTERNAL_EVENTS.MESSAGE, internalHandler$1);
this.trackMessageHandler(targetEventName, internalHandler$1);
return () => {
this.off(INTERNAL_EVENTS.MESSAGE, internalHandler$1);
const handlers = this.messageHandlers.get(targetEventName);
if (handlers) {
const index = handlers.indexOf(internalHandler$1);
if (index > -1) {
handlers.splice(index, 1);
if (handlers.length === 0) this.messageHandlers.delete(targetEventName);
}
}
};
}
const catchAllHandler = eventNameOrHandler;
const catchAllKey = Symbol("catch-all");
const internalHandler = (messageInfo) => {
catchAllHandler(messageInfo.eventName, messageInfo.data, messageInfo.metadata);
};
this.on(INTERNAL_EVENTS.MESSAGE, internalHandler);
this.trackMessageHandler(catchAllKey, internalHandler);
return () => {
this.off(INTERNAL_EVENTS.MESSAGE, internalHandler);
const handlers = this.messageHandlers.get(catchAllKey);
if (handlers) {
const index = handlers.indexOf(internalHandler);
if (index > -1) {
handlers.splice(index, 1);
if (handlers.length === 0) this.messageHandlers.delete(catchAllKey);
}
}
};
}
/**
* Remove message handlers
* @param eventName - The event name to stop listening for
* @returns this instance for chaining
* @warning When called without arguments, removes ALL message handlers from ALL events.
* Prefer using the unsubscribe function returned by onMessage() for targeted cleanup.
* @example
* ```typescript
* // Preferred: use unsubscribe function from onMessage()
* const unsubscribe = eventMsg.onMessage('ping', handler);
* unsubscribe(); // Removes only this handler
*
* // Remove all handlers for a specific event
* eventMsg.offMessage('ping');
*
* // Remove ALL message handlers (use with caution)
* eventMsg.offMessage();
* ```
*/
offMessage(eventName) {
if (eventName) {
const handlers = this.messageHandlers.get(eventName);
if (handlers) {
for (const handler of handlers) this.off(INTERNAL_EVENTS.MESSAGE, handler);
this.messageHandlers.delete(eventName);
}
} else {
for (const [, handlers] of this.messageHandlers) for (const handler of handlers) this.off(INTERNAL_EVENTS.MESSAGE, handler);
this.messageHandlers.clear();
}
return this;
}
/**
* Track message handlers for proper cleanup
*/
trackMessageHandler(key, handler) {
if (!this.messageHandlers.has(key)) this.messageHandlers.set(key, []);
this.messageHandlers.get(key)?.push(handler);
}
/**
* Wait for a single event occurrence with timeout
*/
waitFor(event, options = {}) {
return new Promise((resolve, reject) => {
const timeout = options.timeout ?? this.config.messageTimeout;
const pendingWait = {
resolve,
reject,
timeout: setTimeout(() => {
this.removePendingWait(event, pendingWait);
reject(new WaitForTimeoutError(event, timeout));
}, timeout),
filter: options.filter ?? void 0
};
this.addPendingWait(event, pendingWait);
this.logger.debug("Waiting for event", {
event,
timeout,
hasFilter: !!options.filter,
pendingWaits: this.pendingWaits.get(event)?.length ?? 0
});
});
}
/**
* Get local device address from transport
*/
getLocalAddress() {
return this.transport.getLocalAddress();
}
/**
* Get local group address from transport
*/
getGroupAddress() {
return this.transport.getGroupAddress();
}
/**
* Get connection statistics
*/
getStats() {
return {
...this.stats,
uptime: this.connectTime ? Date.now() - this.connectTime.getTime() : 0
};
}
/**
* Get the underlying transport instance
*/
getTransport() {
return this.transport;
}
/**
* Get the connected device from transport
*/
getDevice() {
return this.transport.getDevice();
}
/**
* Create message header from send options
*/
createHeader(options) {
return {
senderId: this.transport.getLocalAddress(),
receiverId: options.receiverId,
senderGroupId: this.transport.getGroupAddress(),
receiverGroupId: options.receiverGroupId ?? 0,
flags: options.flags ?? 0,
messageId: ++this.messageCounter & 65535
};
}
/**
* Validate send options
*/
validateSendOptions(options) {
if (options.receiverId < 0 || options.receiverId > 255 || !Number.isInteger(options.receiverId)) throw new AddressValidationError(options.receiverId, "receiverId");
if (options.receiverGroupId !== void 0 && (options.receiverGroupId < 0 || options.receiverGroupId > 255 || !Number.isInteger(options.receiverGroupId))) throw new AddressValidationError(options.receiverGroupId, "receiverGroupId");
if (options.flags !== void 0 && (options.flags < 0 || options.flags > 255 || !Number.isInteger(options.flags))) throw new ValidationError("flags must be 0-255", { context: {
field: "flags",
value: options.flags
} });
}
/**
* Setup transport event handlers
*/
setupTransportHandlers() {
this.transport.on("data", this.handleIncomingData.bind(this));
this.transport.on("error", this.handleTransportError.bind(this));
this.transport.on("disconnect", this.handleTransportDisconnect.bind(this));
}
/**
* Handle incoming transport data
*/
handleIncomingData(data) {
try {
const decoded = this.protocol.decode(data);
const metadata = {
senderId: decoded.header.senderId,
senderGroupId: decoded.header.senderGroupId,
receiverId: decoded.header.receiverId,
receiverGroupId: decoded.header.receiverGroupId,
messageId: decoded.header.messageId,
flags: decoded.header.flags,
timestamp: /* @__PURE__ */ new Date()
};
let eventData;
try {
eventData = JSON.parse(decoded.eventData);
} catch {
eventData = decoded.eventData;
}
const messageResult = {
data: eventData,
metadata
};
this.stats.messagesReceived++;
this.logger.info("Message received", {
event: decoded.eventName,
senderId: metadata.senderId,
senderGroupId: metadata.senderGroupId,
messageId: metadata.messageId,
size: data.length,
totalReceived: this.stats.messagesReceived
});
this.logger.trace("Received message hex dump", { hex: hexDump(data) });
this.resolvePendingWaits(decoded.eventName, messageResult);
const messageInfo = {
eventName: decoded.eventName,
data: eventData,
metadata,
messageSize: data.length
};
this.emit(INTERNAL_EVENTS.MESSAGE, messageInfo);
} catch (error) {
this.logger.error("Failed to process incoming message", {
error: error instanceof Error ? error.message : String(error),
messageLength: data.length,
cause: error instanceof Error ? error.name : "Unknown",
hex: hexDump(data, 64)
});
const protocolError = new TransportError("Failed to process incoming message", {
context: { messageLength: data.length },
cause: error instanceof Error ? error : new Error(String(error))
});
this.emit(INTERNAL_EVENTS.ERROR, protocolError);
}
}
/**
* Handle transport errors
*/
handleTransportError(error) {
this.logger.error("Transport error occurred", {
error: error.message,
cause: error.name,
connected: this.connectionState,
uptime: this.connectTime ? Date.now() - this.connectTime.getTime() : 0
});
const transportError = new TransportError("Transport error occurred", { cause: error });
this.stats.lastError = /* @__PURE__ */ new Date();
this.emit(INTERNAL_EVENTS.ERROR, transportError);
}
/**
* Handle transport disconnection
*/
handleTransportDisconnect() {
const uptime = this.connectTime ? Date.now() - this.connectTime.getTime() : 0;
this.logger.warn("Transport disconnected unexpectedly", {
uptime,
messagesSent: this.stats.messagesSent,
messagesReceived: this.stats.messagesReceived,
pendingWaits: this.pendingWaits.size
});
this.connectionState = false;
this.connectTime = void 0;
this.stats.connected = false;
this.clearAllPendingWaits();
this.emit(INTERNAL_EVENTS.DISCONNECT);
}
/**
* Add a pending wait for an event
*/
addPendingWait(event, pendingWait) {
if (!this.pendingWaits.has(event)) this.pendingWaits.set(event, []);
this.pendingWaits.get(event)?.push(pendingWait);
}
/**
* Remove a pending wait
*/
removePendingWait(event, pendingWait) {
const waits = this.pendingWaits.get(event);
if (waits) {
const index = waits.indexOf(pendingWait);
if (index !== -1) {
clearTimeout(pendingWait.timeout);
waits.splice(index, 1);
if (waits.length === 0) this.pendingWaits.delete(event);
}
}
}
/**
* Resolve pending waits for an event
*/
resolvePendingWaits(event, messageResult) {
const waits = this.pendingWaits.get(event);
if (!waits || waits.length === 0) return;
const waitsToResolve = [...waits];
for (const wait of waitsToResolve) {
if (wait.filter && !wait.filter(messageResult.metadata)) continue;
this.removePendingWait(event, wait);
wait.resolve(messageResult);
}
}
/**
* Clear all pending waits (on disconnect)
*/
clearAllPendingWaits() {
for (const [, waits] of this.pendingWaits.entries()) for (const wait of waits) {
clearTimeout(wait.timeout);
wait.reject(new DisconnectionError("Connection lost while waiting for event"));
}
this.pendingWaits.clear();
}
};
//#endregion
export { EventMsg };
//# sourceMappingURL=event-msg.js.map