UNPKG

@eventmsg/core

Version:

EventMsgV3 TypeScript library - Core protocol implementation with transport abstraction

552 lines (550 loc) 17.3 kB
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