UNPKG

@eventmsg/core

Version:

EventMsgV3 TypeScript library - Core protocol implementation with transport abstraction

339 lines (337 loc) 14 kB
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