@eventmsg/core
Version:
EventMsgV3 TypeScript library - Core protocol implementation with transport abstraction
339 lines (337 loc) • 14 kB
JavaScript
const require_protocol_error = require('./errors/protocol-error.cjs');
const require_validation_error = require('./errors/validation-error.cjs');
const require_logger = require('./internal/logger.cjs');
const require_byte_stuffing = require('./internal/byte-stuffing.cjs');
const require_config = require('./types/config.cjs');
//#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 = {
...require_config.DEFAULT_PROTOCOL_OPTIONS,
...config.protocol
};
this.encoding = config.encoding ?? "utf8";
this.logger = require_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 = require_byte_stuffing.stuff(headerBytes);
const stuffedEventName = require_byte_stuffing.stuff(eventNameBytes);
const stuffedEventData = require_byte_stuffing.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 require_validation_error.MessageSizeError(totalSize, this.options.maxEventDataLength + 1e3, "encoded message");
const message = new Uint8Array(totalSize);
let offset = 0;
message[offset++] = require_byte_stuffing.CONTROL_CHARS.SOH;
message.set(stuffedHeader, offset);
offset += stuffedHeader.length;
message[offset++] = require_byte_stuffing.CONTROL_CHARS.STX;
message.set(stuffedEventName, offset);
offset += stuffedEventName.length;
message[offset++] = require_byte_stuffing.CONTROL_CHARS.US;
message.set(stuffedEventData, offset);
offset += stuffedEventData.length;
message[offset++] = require_byte_stuffing.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: require_logger.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 require_validation_error.MessageSizeError) throw error;
throw new require_protocol_error.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 require_validation_error.MessageSizeError(eventData.length, this.options.maxEventDataLength, "eventData");
const eventNameBytes = this.stringToBytes(eventName);
const headerBytes = this.serializeHeader(header);
const stuffedHeader = require_byte_stuffing.stuff(headerBytes);
const stuffedEventName = require_byte_stuffing.stuff(eventNameBytes);
const stuffedEventData = require_byte_stuffing.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 require_validation_error.MessageSizeError(totalSize, this.options.maxEventDataLength + 1e3, "encoded message");
const message = new Uint8Array(totalSize);
let offset = 0;
message[offset++] = require_byte_stuffing.CONTROL_CHARS.SOH;
message.set(stuffedHeader, offset);
offset += stuffedHeader.length;
message[offset++] = require_byte_stuffing.CONTROL_CHARS.STX;
message.set(stuffedEventName, offset);
offset += stuffedEventName.length;
message[offset++] = require_byte_stuffing.CONTROL_CHARS.US;
message.set(stuffedEventData, offset);
offset += stuffedEventData.length;
message[offset++] = require_byte_stuffing.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: require_logger.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 require_validation_error.MessageSizeError) throw error;
throw new require_protocol_error.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: require_logger.hexDump(data) });
try {
if (data.length < 10) throw new require_protocol_error.InvalidMessageError(`Message too short: ${data.length} bytes (minimum 10)`, { context: { messageLength: data.length } });
if (data[0] !== require_byte_stuffing.CONTROL_CHARS.SOH) throw new require_protocol_error.InvalidMessageError(`Message must start with SOH (0x01), got 0x${data[0]?.toString(16).padStart(2, "0")}`, { context: { firstByte: data[0] } });
if (data.at(-1) !== require_byte_stuffing.CONTROL_CHARS.EOT) throw new require_protocol_error.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, require_byte_stuffing.CONTROL_CHARS.STX, 1);
const usIndex = this.findControlChar(data, require_byte_stuffing.CONTROL_CHARS.US, stxIndex + 1);
if (stxIndex === -1) throw new require_protocol_error.InvalidMessageError("STX (0x02) not found in message");
if (usIndex === -1) throw new require_protocol_error.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 = require_byte_stuffing.unstuff(stuffedHeader);
const eventNameBytes = require_byte_stuffing.unstuff(stuffedEventName);
const eventDataBytes = require_byte_stuffing.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: require_logger.hexDump(data, 64)
});
if (error instanceof require_protocol_error.InvalidMessageError) throw error;
throw new require_protocol_error.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 (byte === void 0) continue;
if (escaped) escaped = false;
else if (byte === require_byte_stuffing.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 require_protocol_error.InvalidMessageError(`Header must be exactly 7 bytes, got ${bytes.length}`, { context: { headerLength: bytes.length } });
if (bytes[0] === void 0 || bytes[1] === void 0 || bytes[2] === void 0 || bytes[3] === void 0 || bytes[4] === void 0 || bytes[5] === void 0 || bytes[6] === void 0) throw new require_protocol_error.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 require_validation_error.MessageSizeError(field.value, 255, field.name, { context: { validRange: "0-255" } });
if (header.messageId < 0 || header.messageId > 65535 || !Number.isInteger(header.messageId)) throw new require_validation_error.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 require_validation_error.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 require_validation_error.MessageSizeError(bytes.length, this.options.maxEventDataLength, "eventData");
}
};
//#endregion
exports.Protocol = Protocol;
//# sourceMappingURL=protocol.cjs.map