@eventmsg/core
Version:
EventMsgV3 TypeScript library - Core protocol implementation with transport abstraction
1,615 lines (1,600 loc) • 50.4 kB
JavaScript
//#region rolldown:runtime
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
const eventemitter3 = __toESM(require("eventemitter3"));
const consola_basic = __toESM(require("consola/basic"));
//#region src/errors/event-msg-error.ts
/**
* Base error class for all EventMsg-related errors.
* Provides consistent error handling with context and solutions.
*/
var EventMsgError = class extends Error {
/** Error code for programmatic handling */
code;
/** Additional context about the error */
context;
/** Suggested solutions for the error */
solutions;
/** Original error that caused this error */
cause;
constructor(message, code, options = {}) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.context = options.context;
this.solutions = options.solutions || [];
this.cause = options.cause;
if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
}
/**
* Get a detailed error message including context and solutions
*/
getDetailedMessage() {
let details = `${this.name}: ${this.message}`;
if (this.code) details += ` (${this.code})`;
if (this.context && Object.keys(this.context).length > 0) {
details += "\nContext:";
for (const [key, value] of Object.entries(this.context)) details += `\n ${key}: ${JSON.stringify(value)}`;
}
if (this.solutions.length > 0) {
details += "\nSuggested solutions:";
for (const solution of this.solutions) details += `\n • ${solution}`;
}
if (this.cause) details += `\nCaused by: ${this.cause.message}`;
return details;
}
/**
* Convert error to JSON for logging/serialization
*/
toJSON() {
return {
name: this.name,
message: this.message,
code: this.code,
context: this.context,
solutions: this.solutions,
stack: this.stack,
cause: this.cause ? {
name: this.cause.name,
message: this.cause.message
} : void 0
};
}
};
//#endregion
//#region src/errors/protocol-error.ts
/**
* Error related to EventMsgV3 protocol operations (encoding, decoding, validation)
*/
var ProtocolError = class extends EventMsgError {
constructor(message, options = {}) {
super(message, "PROTOCOL_ERROR", {
...options,
solutions: options.solutions || [
"Check message format",
"Verify protocol specification compliance",
"Review message content"
]
});
}
};
/**
* Message encoding failed
*/
var EncodingError = class extends ProtocolError {
constructor(message = "Failed to encode message", options = {}) {
super(message, {
...options,
solutions: [
"Check event name length (≤64 bytes)",
"Check event data length (≤3048 bytes)",
"Verify data is JSON serializable",
"Check for invalid characters"
]
});
this.code = "ENCODING_ERROR";
}
};
/**
* Message decoding failed
*/
var DecodingError = class extends ProtocolError {
constructor(message = "Failed to decode message", options = {}) {
super(message, {
...options,
solutions: [
"Check message framing (SOH, STX, US, EOT)",
"Verify byte stuffing is correct",
"Check for data corruption",
"Ensure sender uses same protocol version"
]
});
this.code = "DECODING_ERROR";
}
};
/**
* Message validation failed
*/
var ValidationError = class extends ProtocolError {
constructor(message = "Message validation failed", options = {}) {
super(message, {
...options,
solutions: [
"Check address ranges (0-255)",
"Verify message size limits",
"Check required fields are present",
"Validate data types"
]
});
this.code = "VALIDATION_ERROR";
}
};
/**
* Invalid message format
*/
var InvalidMessageError = class extends ProtocolError {
constructor(message = "Invalid message format", options = {}) {
super(message, {
...options,
solutions: [
"Check message starts with SOH (0x01)",
"Check message ends with EOT (0x04)",
"Verify minimum message length (10 bytes)",
"Check header is exactly 7 bytes after unstuffing"
]
});
this.code = "INVALID_MESSAGE_ERROR";
}
};
//#endregion
//#region src/errors/timeout-error.ts
/**
* Error for operations that exceed their timeout duration
*/
var TimeoutError = class extends EventMsgError {
/** Timeout duration in milliseconds */
timeoutMs;
/** Operation that timed out */
operation;
constructor(message, timeoutMs, operation = "operation", options = {}) {
super(message, "TIMEOUT_ERROR", {
...options,
context: {
...options.context,
timeoutMs,
operation
},
solutions: [
"Increase timeout duration",
"Check network connectivity",
"Verify target device is responding",
"Reduce message frequency"
]
});
this.timeoutMs = timeoutMs;
this.operation = operation;
}
};
/**
* Send operation timed out
*/
var SendTimeoutError = class extends TimeoutError {
constructor(timeoutMs, options = {}) {
super(`Send operation timed out after ${timeoutMs}ms`, timeoutMs, "send", {
...options,
solutions: [
"Increase send timeout",
"Check transport connection",
"Verify target device is reachable",
"Check for network congestion"
]
});
this.code = "SEND_TIMEOUT_ERROR";
}
};
/**
* WaitFor operation timed out
*/
var WaitForTimeoutError = class extends TimeoutError {
/** Event name that was being waited for */
eventName;
constructor(eventName, timeoutMs, options = {}) {
super(`waitFor('${eventName}') timed out after ${timeoutMs}ms`, timeoutMs, "waitFor", {
...options,
context: {
...options.context,
eventName
},
solutions: [
"Increase waitFor timeout",
"Check if event name is correct",
"Verify sender is responding",
"Check event filter conditions"
]
});
this.eventName = eventName;
this.code = "WAIT_FOR_TIMEOUT_ERROR";
}
};
/**
* Connection timeout error
*/
var ConnectionTimeoutError = class extends TimeoutError {
constructor(timeoutMs, options = {}) {
super(`Connection attempt timed out after ${timeoutMs}ms`, timeoutMs, "connect", {
...options,
solutions: [
"Increase connection timeout",
"Check device availability",
"Verify connection parameters",
"Check for interference"
]
});
this.code = "CONNECTION_TIMEOUT_ERROR";
}
};
//#endregion
//#region src/errors/transport-error.ts
/**
* Error related to transport layer operations (connection, sending, receiving)
*/
var TransportError = class extends EventMsgError {
constructor(message, options = {}) {
super(message, "TRANSPORT_ERROR", {
...options,
solutions: options.solutions || [
"Check transport connection",
"Verify transport configuration",
"Retry the operation"
]
});
}
};
/**
* Transport connection failed
*/
var ConnectionError = class extends TransportError {
constructor(message = "Failed to connect to transport", options = {}) {
super(message, {
...options,
solutions: [
"Check if the device is available",
"Verify connection parameters",
"Ensure no other process is using the transport",
"Check device permissions"
]
});
this.code = "CONNECTION_ERROR";
}
};
/**
* Transport send operation failed
*/
var SendError = class extends TransportError {
constructor(message = "Failed to send data through transport", options = {}) {
super(message, {
...options,
solutions: [
"Check transport connection status",
"Verify message size is within limits",
"Retry sending the message",
"Check for transport buffer overflow"
]
});
this.code = "SEND_ERROR";
}
};
/**
* Transport disconnection error
*/
var DisconnectionError = class extends TransportError {
constructor(message = "Transport disconnected unexpectedly", options = {}) {
super(message, {
...options,
solutions: [
"Check physical connection",
"Implement reconnection logic",
"Monitor connection status",
"Handle graceful disconnection"
]
});
this.code = "DISCONNECTION_ERROR";
}
};
//#endregion
//#region src/errors/validation-error.ts
/**
* Error for input validation failures
*/
var ValidationError$1 = class extends EventMsgError {
/** Field that failed validation */
field;
/** Value that was invalid */
value;
constructor(message, options = {}) {
super(message, "VALIDATION_ERROR", {
...options,
context: {
...options.context,
field: options.field,
value: options.value
},
solutions: [
"Check input parameters",
"Verify value ranges and types",
"Review API documentation"
]
});
this.field = options.field;
this.value = options.value;
}
};
/**
* Address validation error (0-255 range)
*/
var AddressValidationError = class extends ValidationError$1 {
constructor(address, field = "address", options = {}) {
super(`Invalid ${field}: ${address}. Must be 0-255`, {
...options,
field,
value: address,
solutions: [
"Use address in range 0-255",
"Check for negative values",
"Verify address is an integer"
]
});
this.code = "ADDRESS_VALIDATION_ERROR";
}
};
/**
* Message size validation error
*/
var MessageSizeError = class extends ValidationError$1 {
constructor(size, maxSize, field = "message", options = {}) {
super(`${field} size ${size} bytes exceeds maximum ${maxSize} bytes`, {
...options,
field,
value: size,
context: {
...options.context,
maxSize
},
solutions: [
`Reduce ${field} size to ${maxSize} bytes or less`,
"Consider splitting large messages",
"Use more efficient encoding"
]
});
this.code = "MESSAGE_SIZE_ERROR";
}
};
//#endregion
//#region src/types/logger.ts
/**
* Default logging configuration
*/
const DEFAULT_LOGGING_CONFIG = {
enabled: true,
level: 3
};
/**
* Log namespaces for tagging
*/
const LOG_NAMESPACES = {
CORE: "eventmsg:core",
PROTOCOL: "eventmsg:protocol",
TRANSPORT: "eventmsg:transport",
TRANSPORT_WEBBLE: "eventmsg:transport:webble"
};
//#endregion
//#region src/internal/logger.ts
let globalConsola = (0, consola_basic.createConsola)({ level: DEFAULT_LOGGING_CONFIG.level ?? 3 });
/**
* Configure global logging
*/
function configureLogging(config) {
const finalConfig = {
...DEFAULT_LOGGING_CONFIG,
...config
};
if (finalConfig.enabled && finalConfig.level !== -999) globalConsola = (0, consola_basic.createConsola)({ level: finalConfig.level ?? 3 });
else globalConsola = (0, consola_basic.createConsola)({
level: 0,
reporters: []
});
}
/**
* Get logger for a component
*/
function getLogger(namespace) {
return globalConsola.withTag(LOG_NAMESPACES[namespace]);
}
/**
* Simple hex dump for debugging
*/
function hexDump(data, maxBytes = 64) {
const limited = data.length > maxBytes ? data.slice(0, maxBytes) : data;
const hex = Array.from(limited).map((b) => b.toString(16).padStart(2, "0")).join(" ");
if (data.length > maxBytes) return `${hex} ... (${data.length - maxBytes} more bytes)`;
return hex;
}
//#endregion
//#region src/internal/byte-stuffing.ts
/**
* EventMsgV3 Byte Stuffing Implementation
*
* Implements the exact byte stuffing algorithm from the C implementation
* to ensure protocol compatibility.
*/
/**
* Control characters used in EventMsgV3 protocol
*/
const CONTROL_CHARS = {
SOH: 1,
STX: 2,
EOT: 4,
US: 31,
ESC: 27
};
/**
* XOR mask for byte stuffing
*/
const STUFF_MASK = 32;
/**
* Check if a byte is a control character that needs stuffing
*/
function isControlChar(byte) {
return byte === CONTROL_CHARS.SOH || byte === CONTROL_CHARS.STX || byte === CONTROL_CHARS.EOT || byte === CONTROL_CHARS.US || byte === CONTROL_CHARS.ESC;
}
/**
* Stuff (encode) bytes by escaping control characters
*
* Algorithm:
* - If byte is a control character: Insert ESC, then XOR byte with 0x20
* - Otherwise: Insert byte as-is
*
* @param data Input data to stuff
* @returns Stuffed data
*/
function stuff(data) {
const result = [];
for (let i = 0; i < data.length; i++) {
const byte = data[i];
if (isControlChar(byte)) {
result.push(CONTROL_CHARS.ESC);
result.push(byte ^ STUFF_MASK);
} else result.push(byte);
}
return new Uint8Array(result);
}
/**
* Unstuff (decode) bytes by handling escaped characters
*
* Algorithm:
* - If byte is ESC: Mark next byte as escaped, continue
* - If previous byte was ESC: XOR current byte with 0x20, add to output
* - Otherwise: Add byte as-is to output
*
* @param data Stuffed data to unstuff
* @returns Unstuffed data
* @throws {Error} If data contains invalid escape sequences
*/
function unstuff(data) {
const result = [];
let escaped = false;
for (let i = 0; i < data.length; i++) {
const byte = data[i];
if (escaped) {
result.push(byte ^ STUFF_MASK);
escaped = false;
} else if (byte === CONTROL_CHARS.ESC) escaped = true;
else result.push(byte);
}
if (escaped) throw new Error("Invalid byte stuffing: data ends with incomplete escape sequence");
return new Uint8Array(result);
}
/**
* Calculate the maximum possible size after stuffing
* In worst case, every byte could be a control character, doubling the size
*
* @param originalSize Original data size
* @returns Maximum size after stuffing
*/
function getMaxStuffedSize(originalSize) {
return originalSize * 2;
}
/**
* Calculate the minimum possible size after unstuffing
* In best case, no bytes are stuffed
*
* @param stuffedSize Stuffed data size
* @returns Minimum size after unstuffing
*/
function getMinUnstuffedSize(stuffedSize) {
return Math.floor(stuffedSize / 2);
}
/**
* Test if data contains any control characters that would need stuffing
*
* @param data Data to test
* @returns True if data contains control characters
*/
function needsStuffing(data) {
for (let i = 0; i < data.length; i++) if (isControlChar(data[i])) return true;
return false;
}
/**
* Validate that stuffed data has proper escape sequences
*
* @param data Stuffed data to validate
* @returns True if data has valid stuffing
*/
function isValidStuffing(data) {
let escaped = false;
for (let i = 0; i < data.length; i++) {
const byte = data[i];
if (escaped) {
const unescaped = byte ^ STUFF_MASK;
if (!isControlChar(unescaped)) return false;
escaped = false;
} else if (byte === CONTROL_CHARS.ESC) escaped = true;
else if (isControlChar(byte)) return false;
}
return !escaped;
}
//#endregion
//#region src/types/config.ts
/**
* Default protocol options
*/
const DEFAULT_PROTOCOL_OPTIONS = {
strictValidation: true,
maxEventNameLength: 64,
maxEventDataLength: 3048
};
/**
* Default configuration values
*/
const DEFAULT_CONFIG = {
maxMessageSize: 4096,
messageTimeout: 5e3,
encoding: "utf8",
logging: DEFAULT_LOGGING_CONFIG,
protocol: DEFAULT_PROTOCOL_OPTIONS
};
//#endregion
//#region src/protocol.ts
/**
* EventMsgV3 Protocol Implementation
*
* Handles encoding and decoding of EventMsgV3 binary protocol messages.
* Message format: [SOH][StuffedHeader][STX][StuffedEventName][US][StuffedEventData][EOT]
*/
var Protocol = class {
options;
encoding;
logger;
constructor(config) {
this.options = {
...DEFAULT_PROTOCOL_OPTIONS,
...config.protocol
};
this.encoding = config.encoding ?? "utf8";
this.logger = getLogger("PROTOCOL");
}
/**
* Encode a message into EventMsgV3 binary format
*
* @param header Message header (7 bytes)
* @param eventName Event name string
* @param eventData Event data string (JSON)
* @returns Encoded binary message
* @throws {EncodingError} If encoding fails
* @throws {MessageSizeError} If message exceeds size limits
*/
encode(header, eventName, eventData) {
this.logger.debug("Starting message encode", {
eventName,
eventDataLength: eventData.length,
header,
encoding: this.encoding
});
try {
this.validateHeader(header);
this.validateEventName(eventName);
this.validateEventData(eventData);
const eventNameBytes = this.stringToBytes(eventName);
const eventDataBytes = this.stringToBytes(eventData);
const headerBytes = this.serializeHeader(header);
const stuffedHeader = stuff(headerBytes);
const stuffedEventName = stuff(eventNameBytes);
const stuffedEventData = stuff(eventDataBytes);
this.logger.trace("Byte stuffing complete", {
headerSize: `${headerBytes.length} -> ${stuffedHeader.length}`,
nameSize: `${eventNameBytes.length} -> ${stuffedEventName.length}`,
dataSize: `${eventDataBytes.length} -> ${stuffedEventData.length}`
});
const totalSize = 1 + stuffedHeader.length + 1 + stuffedEventName.length + 1 + stuffedEventData.length + 1;
if (totalSize > this.options.maxEventDataLength + 1e3) throw new MessageSizeError(totalSize, this.options.maxEventDataLength + 1e3, "encoded message");
const message = new Uint8Array(totalSize);
let offset = 0;
message[offset++] = CONTROL_CHARS.SOH;
message.set(stuffedHeader, offset);
offset += stuffedHeader.length;
message[offset++] = CONTROL_CHARS.STX;
message.set(stuffedEventName, offset);
offset += stuffedEventName.length;
message[offset++] = CONTROL_CHARS.US;
message.set(stuffedEventData, offset);
offset += stuffedEventData.length;
message[offset++] = CONTROL_CHARS.EOT;
const finalMessage = message.subarray(0, offset);
this.logger.info("Message encoded successfully", {
eventName,
finalSize: finalMessage.length,
messageId: header.messageId
});
this.logger.trace("Encoded message hex dump", { hex: hexDump(finalMessage) });
return finalMessage;
} catch (error) {
this.logger.error("Message encoding failed", {
error: error instanceof Error ? error.message : String(error),
eventName,
eventDataLength: eventData.length,
cause: error instanceof Error ? error.name : "Unknown"
});
if (error instanceof MessageSizeError) throw error;
throw new EncodingError("Failed to encode EventMsgV3 message", {
context: {
eventName,
eventDataLength: eventData.length
},
cause: error instanceof Error ? error : new Error(String(error))
});
}
}
/**
* Encode a message with binary data into EventMsgV3 format
*
* @param header Message header (7 bytes)
* @param eventName Event name string
* @param eventData Binary event data
* @returns Encoded binary message
* @throws {EncodingError} If encoding fails
* @throws {MessageSizeError} If message exceeds size limits
*/
encodeBinary(header, eventName, eventData) {
this.logger.debug("Starting binary message encode", {
eventName,
eventDataLength: eventData.length,
header
});
try {
this.validateHeader(header);
this.validateEventName(eventName);
if (eventData.length > this.options.maxEventDataLength) throw new MessageSizeError(eventData.length, this.options.maxEventDataLength, "eventData");
const eventNameBytes = this.stringToBytes(eventName);
const headerBytes = this.serializeHeader(header);
const stuffedHeader = stuff(headerBytes);
const stuffedEventName = stuff(eventNameBytes);
const stuffedEventData = stuff(eventData);
this.logger.trace("Binary byte stuffing complete", {
headerSize: `${headerBytes.length} -> ${stuffedHeader.length}`,
nameSize: `${eventNameBytes.length} -> ${stuffedEventName.length}`,
dataSize: `${eventData.length} -> ${stuffedEventData.length}`
});
const totalSize = 1 + stuffedHeader.length + 1 + stuffedEventName.length + 1 + stuffedEventData.length + 1;
if (totalSize > this.options.maxEventDataLength + 1e3) throw new MessageSizeError(totalSize, this.options.maxEventDataLength + 1e3, "encoded message");
const message = new Uint8Array(totalSize);
let offset = 0;
message[offset++] = CONTROL_CHARS.SOH;
message.set(stuffedHeader, offset);
offset += stuffedHeader.length;
message[offset++] = CONTROL_CHARS.STX;
message.set(stuffedEventName, offset);
offset += stuffedEventName.length;
message[offset++] = CONTROL_CHARS.US;
message.set(stuffedEventData, offset);
offset += stuffedEventData.length;
message[offset++] = CONTROL_CHARS.EOT;
const finalMessage = message.subarray(0, offset);
this.logger.info("Binary message encoded successfully", {
eventName,
finalSize: finalMessage.length,
messageId: header.messageId
});
this.logger.trace("Encoded binary message hex dump", { hex: hexDump(finalMessage) });
return finalMessage;
} catch (error) {
this.logger.error("Binary message encoding failed", {
error: error instanceof Error ? error.message : String(error),
eventName,
eventDataLength: eventData.length,
cause: error instanceof Error ? error.name : "Unknown"
});
if (error instanceof MessageSizeError) throw error;
throw new EncodingError("Failed to encode EventMsgV3 binary message", {
context: {
eventName,
eventDataLength: eventData.length
},
cause: error instanceof Error ? error : new Error(String(error))
});
}
}
/**
* Decode a binary message into EventMsgV3 components
*
* @param data Binary message data
* @returns Decoded message components
* @throws {DecodingError} If decoding fails
* @throws {InvalidMessageError} If message format is invalid
*/
decode(data) {
this.logger.debug("Starting message decode", {
messageLength: data.length,
encoding: this.encoding
});
this.logger.trace("Incoming message hex dump", { hex: hexDump(data) });
try {
if (data.length < 10) throw new InvalidMessageError(`Message too short: ${data.length} bytes (minimum 10)`, { context: { messageLength: data.length } });
if (data[0] !== CONTROL_CHARS.SOH) throw new InvalidMessageError(`Message must start with SOH (0x01), got 0x${data[0]?.toString(16).padStart(2, "0")}`, { context: { firstByte: data[0] } });
if (data.at(-1) !== CONTROL_CHARS.EOT) throw new InvalidMessageError(`Message must end with EOT (0x04), got 0x${data.at(-1)?.toString(16).padStart(2, "0")}`, { context: { lastByte: data.at(-1) } });
const stxIndex = this.findControlChar(data, CONTROL_CHARS.STX, 1);
const usIndex = this.findControlChar(data, CONTROL_CHARS.US, stxIndex + 1);
if (stxIndex === -1) throw new InvalidMessageError("STX (0x02) not found in message");
if (usIndex === -1) throw new InvalidMessageError("US (0x1F) not found in message");
const stuffedHeader = data.subarray(1, stxIndex);
const stuffedEventName = data.subarray(stxIndex + 1, usIndex);
const stuffedEventData = data.subarray(usIndex + 1, data.length - 1);
const headerBytes = unstuff(stuffedHeader);
const eventNameBytes = unstuff(stuffedEventName);
const eventDataBytes = unstuff(stuffedEventData);
this.logger.trace("Byte unstuffing complete", {
headerSize: `${stuffedHeader.length} -> ${headerBytes.length}`,
nameSize: `${stuffedEventName.length} -> ${eventNameBytes.length}`,
dataSize: `${stuffedEventData.length} -> ${eventDataBytes.length}`
});
const header = this.deserializeHeader(headerBytes);
const eventName = this.bytesToString(eventNameBytes);
const eventData = this.bytesToString(eventDataBytes);
if (this.options.strictValidation) {
this.validateHeader(header);
this.validateEventName(eventName);
this.validateEventData(eventData);
}
this.logger.info("Message decoded successfully", {
eventName,
messageId: header.messageId,
senderId: header.senderId,
receiverId: header.receiverId,
eventDataLength: eventData.length
});
return {
header,
eventName,
eventData
};
} catch (error) {
this.logger.error("Message decoding failed", {
error: error instanceof Error ? error.message : String(error),
messageLength: data.length,
cause: error instanceof Error ? error.name : "Unknown",
hex: hexDump(data, 64)
});
if (error instanceof InvalidMessageError) throw error;
throw new DecodingError("Failed to decode EventMsgV3 message", {
context: { messageLength: data.length },
cause: error instanceof Error ? error : new Error(String(error))
});
}
}
/**
* Find a control character in data, skipping escaped instances
*/
findControlChar(data, target, startIndex) {
let escaped = false;
for (let i = startIndex; i < data.length; i++) {
const byte = data[i];
if (escaped) escaped = false;
else if (byte === CONTROL_CHARS.ESC) escaped = true;
else if (byte === target) return i;
}
return -1;
}
/**
* Serialize message header to 7 bytes (big-endian messageId)
*/
serializeHeader(header) {
const bytes = new Uint8Array(7);
bytes[0] = header.senderId;
bytes[1] = header.receiverId;
bytes[2] = header.senderGroupId;
bytes[3] = header.receiverGroupId;
bytes[4] = header.flags;
bytes[5] = header.messageId >> 8 & 255;
bytes[6] = header.messageId & 255;
return bytes;
}
/**
* Deserialize 7 bytes to message header (big-endian messageId)
*/
deserializeHeader(bytes) {
if (bytes.length !== 7) throw new InvalidMessageError(`Header must be exactly 7 bytes, got ${bytes.length}`, { context: { headerLength: bytes.length } });
return {
senderId: bytes[0],
receiverId: bytes[1],
senderGroupId: bytes[2],
receiverGroupId: bytes[3],
flags: bytes[4],
messageId: bytes[5] << 8 | bytes[6]
};
}
/**
* Convert string to bytes using configured encoding
*/
stringToBytes(str) {
if (this.encoding === "utf8") return new TextEncoder().encode(str);
const bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) bytes[i] = str.charCodeAt(i) & 255;
return bytes;
}
/**
* Convert bytes to string using configured encoding
*/
bytesToString(bytes) {
if (this.encoding === "utf8") return new TextDecoder("utf8").decode(bytes);
return String.fromCharCode(...bytes);
}
/**
* Validate message header fields
*/
validateHeader(header) {
const fields = [
{
name: "senderId",
value: header.senderId
},
{
name: "receiverId",
value: header.receiverId
},
{
name: "senderGroupId",
value: header.senderGroupId
},
{
name: "receiverGroupId",
value: header.receiverGroupId
},
{
name: "flags",
value: header.flags
}
];
for (const field of fields) if (field.value < 0 || field.value > 255 || !Number.isInteger(field.value)) throw new MessageSizeError(field.value, 255, field.name, { context: { validRange: "0-255" } });
if (header.messageId < 0 || header.messageId > 65535 || !Number.isInteger(header.messageId)) throw new MessageSizeError(header.messageId, 65535, "messageId", { context: { validRange: "0-65535" } });
}
/**
* Validate event name length
*/
validateEventName(eventName) {
const bytes = this.stringToBytes(eventName);
if (bytes.length > this.options.maxEventNameLength) throw new MessageSizeError(bytes.length, this.options.maxEventNameLength, "eventName");
}
/**
* Validate event data length
*/
validateEventData(eventData) {
const bytes = this.stringToBytes(eventData);
if (bytes.length > this.options.maxEventDataLength) throw new MessageSizeError(bytes.length, this.options.maxEventDataLength, "eventData");
}
};
//#endregion
//#region src/types/base-transport.ts
/**
* Abstract base class for transport implementations.
*
* Provides common functionality like connection state management, error handling,
* and configuration validation while allowing transport-specific implementations
* through the template method pattern.
*
* @template TConfig - Transport-specific configuration type extending TransportConfig
*/
var BaseTransport = class extends eventemitter3.EventEmitter {
config;
connectionState = "disconnected";
connectStartTime;
constructor(config) {
super();
this.validateConfig(config);
this.config = { ...config };
}
/**
* Connect to the transport layer.
* Handles state management and error wrapping around doConnect().
*/
async connect() {
if (this.connectionState === "connected") return;
if (this.connectionState === "connecting") throw new ConnectionError("Connection already in progress", { context: this.getErrorContext("connect") });
this.connectionState = "connecting";
this.connectStartTime = /* @__PURE__ */ new Date();
try {
await this.doConnect();
this.connectionState = "connected";
this.emit("connect");
} catch (error) {
this.connectionState = "disconnected";
this.connectStartTime = void 0;
throw new ConnectionError(`Failed to connect via ${this.constructor.name}`, {
cause: error instanceof Error ? error : new Error(String(error)),
context: this.getErrorContext("connect")
});
}
}
/**
* Disconnect from the transport layer.
* Handles state management and error wrapping around doDisconnect().
*/
async disconnect() {
if (this.connectionState === "disconnected") return;
const wasConnected = this.connectionState === "connected";
try {
await this.doDisconnect();
} catch (error) {
throw new DisconnectionError(`Failed to disconnect from ${this.constructor.name}`, {
cause: error instanceof Error ? error : new Error(String(error)),
context: this.getErrorContext("disconnect")
});
} finally {
this.connectionState = "disconnected";
this.connectStartTime = void 0;
if (wasConnected) this.emit("disconnect");
}
}
/**
* Check if transport is currently connected.
*/
isConnected() {
return this.connectionState === "connected";
}
/**
* Send raw binary data through the transport.
* Validates state and data before calling doSend().
*/
async send(data) {
if (this.connectionState !== "connected") throw new SendError("Cannot send data: transport not connected", { context: this.getErrorContext("send", { dataSize: data.length }) });
if (!(data instanceof Uint8Array)) throw new ValidationError$1("Data must be a Uint8Array", { context: this.getErrorContext("send", { dataType: typeof data }) });
if (data.length === 0) throw new ValidationError$1("Cannot send empty data", { context: this.getErrorContext("send", { dataSize: 0 }) });
try {
await this.doSend(data);
} catch (error) {
throw new SendError(`Failed to send ${data.length} bytes via ${this.constructor.name}`, {
cause: error instanceof Error ? error : new Error(String(error)),
context: this.getErrorContext("send", { dataSize: data.length })
});
}
}
/**
* Get the local device address.
*/
getLocalAddress() {
return this.config.localAddress;
}
/**
* Get the local group address.
*/
getGroupAddress() {
return this.config.groupAddress;
}
/**
* Validate transport configuration.
* @param config - Configuration to validate
* @throws {ValidationError} If configuration is invalid
*/
validateConfig(config) {
if (!config) throw new ValidationError$1("Transport configuration is required");
if (typeof config.localAddress !== "number") throw new ValidationError$1("localAddress must be a number", { context: { localAddress: config.localAddress } });
if (!Number.isInteger(config.localAddress) || config.localAddress < 0 || config.localAddress > 255) throw new ValidationError$1("localAddress must be an integer between 0-255", { context: { localAddress: config.localAddress } });
if (typeof config.groupAddress !== "number") throw new ValidationError$1("groupAddress must be a number", { context: { groupAddress: config.groupAddress } });
if (!Number.isInteger(config.groupAddress) || config.groupAddress < 0 || config.groupAddress > 255) throw new ValidationError$1("groupAddress must be an integer between 0-255", { context: { groupAddress: config.groupAddress } });
}
/**
* Generate error context for consistent error reporting.
* @param operation - Operation being performed
* @param additional - Additional context data
* @returns Error context object
*/
getErrorContext(operation, additional = {}) {
return {
transportType: this.constructor.name,
state: this.connectionState,
operation,
localAddress: this.config.localAddress,
groupAddress: this.config.groupAddress,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
connectedSince: this.connectStartTime?.toISOString(),
...additional
};
}
/**
* Emit a transport error event.
* @param error - Error to emit
*/
emitError(error) {
this.emit("error", error);
}
/**
* Handle unexpected disconnection.
* Should be called by transport implementations when connection is lost.
*/
handleUnexpectedDisconnect(reason) {
const wasConnected = this.connectionState === "connected";
this.connectionState = "disconnected";
this.connectStartTime = void 0;
if (wasConnected) {
this.emit("disconnect");
if (reason) this.emitError(new DisconnectionError("Transport disconnected unexpectedly", {
cause: reason,
context: this.getErrorContext("unexpected_disconnect")
}));
}
}
};
//#endregion
//#region src/types/events.ts
/**
* Internal event names used by EventMsg
*/
const INTERNAL_EVENTS = {
MESSAGE: "message",
CONNECT: "connect",
DISCONNECT: "disconnect",
ERROR: "error",
SEND: "send"
};
//#endregion
//#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 eventemitter3.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;
this.protocol = new Protocol({
...this.config,
transport: this.transport
});
configureLogging(this.config.logging);
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 = (messageInfo) => {
if (messageInfo.eventName === targetEventName) specificHandler(messageInfo.data, messageInfo.metadata);
};
this.on(INTERNAL_EVENTS.MESSAGE, internalHandler);
this.trackMessageHandler(targetEventName, internalHandler);
} else {
const catchAllHandler = eventNameOrHandler;
const internalHandler = (messageInfo) => {
catchAllHandler(messageInfo.eventName, messageInfo.data, messageInfo.metadata);
};
this.on(INTERNAL_EVENTS.MESSAGE, internalHandler);
this.trackMessageHandler(Symbol("catch-all"), internalHandler);
}
return this;
}
/**
* Remove a message handler
* @param eventName - The event name to stop listening for (optional for catch-all handlers)
* @returns this instance for chaining
* @example
* ```typescript
* // Remove specific event handlers
* eventMsg.offMessage('ping');
*
* // Remove all message handlers
* 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
};
}
/**
* 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$1("flags must be 0-255", {
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
exports.AddressValidationError = AddressValidationError;
exports.BaseTransport = BaseTransport;
exports.CONTROL_CHARS = CONTROL_CHARS;
exports.ConnectionError = ConnectionError;
exports.ConnectionTimeoutError = ConnectionTimeoutError;
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
exports.DEFAULT_LOGGING_CONFIG = DEFAULT_LOGGING_CONFIG;
exports.DEFAULT_PROTOCOL_OPTIONS = DEFAULT_PROTOCOL_OPTIONS;
exports.DecodingError = DecodingError;
exports.DisconnectionError = DisconnectionError;
exports.EncodingError = EncodingError;
exports.EventMsg = EventMsg;
exports.EventMsgError = EventMsgError;
exports.INTERNAL_EVENTS = INTERNAL_EVENTS;
exports.InvalidMessageError = InvalidMessageError;
exports.LOG_NAMESPACES = LOG_NAMESPACES;
exports.MessageSizeError = MessageSizeError;
exports.Protocol = Protocol;
exports.ProtocolError = ProtocolError;
exports.ProtocolValidationError = ValidationError;
exports.SendError = SendError;
exports.SendTimeoutError = SendTimeoutError;
exports.TimeoutError = TimeoutError;
exports.TransportError = TransportError