UNPKG

@eventmsg/core

Version:

EventMsgV3 TypeScript library - Core protocol implementation with transport abstraction

857 lines (854 loc) 24.8 kB
import { EventEmitter } from "eventemitter3"; import { createConsola } from "consola/basic"; //#region src/errors/event-msg-error.d.ts /** * Base error class for all EventMsg-related errors. * Provides consistent error handling with context and solutions. */ declare class EventMsgError extends Error { /** Error code for programmatic handling */ code: string; /** Additional context about the error */ readonly context: Record<string, unknown> | undefined; /** Suggested solutions for the error */ readonly solutions: string[]; /** Original error that caused this error */ readonly cause: Error | undefined; constructor(message: string, code: string, options?: { context?: Record<string, unknown>; solutions?: string[]; cause?: Error; }); /** * Get a detailed error message including context and solutions */ getDetailedMessage(): string; /** * Convert error to JSON for logging/serialization */ toJSON(): Record<string, unknown>; } //#endregion //#region src/errors/protocol-error.d.ts /** * Error related to EventMsgV3 protocol operations (encoding, decoding, validation) */ declare class ProtocolError extends EventMsgError { constructor(message: string, options?: { context?: Record<string, unknown>; solutions?: string[]; cause?: Error; }); } /** * Message encoding failed */ declare class EncodingError extends ProtocolError { constructor(message?: string, options?: { context?: Record<string, unknown>; cause?: Error; }); } /** * Message decoding failed */ declare class DecodingError extends ProtocolError { constructor(message?: string, options?: { context?: Record<string, unknown>; cause?: Error; }); } /** * Message validation failed */ declare class ValidationError extends ProtocolError { constructor(message?: string, options?: { context?: Record<string, unknown>; cause?: Error; }); } /** * Invalid message format */ declare class InvalidMessageError extends ProtocolError { constructor(message?: string, options?: { context?: Record<string, unknown>; cause?: Error; }); } //#endregion //#region src/errors/timeout-error.d.ts /** * Error for operations that exceed their timeout duration */ declare class TimeoutError extends EventMsgError { /** Timeout duration in milliseconds */ readonly timeoutMs: number; /** Operation that timed out */ readonly operation: string; constructor(message: string, timeoutMs: number, operation?: string, options?: { context?: Record<string, unknown>; solutions?: string[]; cause?: Error; }); } /** * Send operation timed out */ declare class SendTimeoutError extends TimeoutError { constructor(timeoutMs: number, options?: { context?: Record<string, unknown>; cause?: Error; }); } /** * WaitFor operation timed out */ declare class WaitForTimeoutError extends TimeoutError { /** Event name that was being waited for */ readonly eventName: string; constructor(eventName: string, timeoutMs: number, options?: { context?: Record<string, unknown>; cause?: Error; }); } /** * Connection timeout error */ declare class ConnectionTimeoutError extends TimeoutError { constructor(timeoutMs: number, options?: { context?: Record<string, unknown>; cause?: Error; }); } //#endregion //#region src/errors/transport-error.d.ts /** * Error related to transport layer operations (connection, sending, receiving) */ declare class TransportError extends EventMsgError { constructor(message: string, options?: { context?: Record<string, unknown>; solutions?: string[]; cause?: Error; }); } /** * Transport connection failed */ declare class ConnectionError extends TransportError { constructor(message?: string, options?: { context?: Record<string, unknown>; cause?: Error; }); } /** * Transport send operation failed */ declare class SendError extends TransportError { constructor(message?: string, options?: { context?: Record<string, unknown>; cause?: Error; }); } /** * Transport disconnection error */ declare class DisconnectionError extends TransportError { constructor(message?: string, options?: { context?: Record<string, unknown>; cause?: Error; }); } //#endregion //#region src/errors/validation-error.d.ts /** * Error for input validation failures */ declare class ValidationError$1 extends EventMsgError { /** Field that failed validation */ readonly field: string | undefined; /** Value that was invalid */ readonly value: unknown; constructor(message: string, options?: { field?: string; value?: unknown; context?: Record<string, unknown>; solutions?: string[]; cause?: Error; }); } /** * Address validation error (0-255 range) */ declare class AddressValidationError extends ValidationError$1 { constructor(address: number, field?: string, options?: { context?: Record<string, unknown>; solutions?: string[]; cause?: Error; }); } /** * Message size validation error */ declare class MessageSizeError extends ValidationError$1 { constructor(size: number, maxSize: number, field?: string, options?: { context?: Record<string, unknown>; solutions?: string[]; cause?: Error; }); } //#endregion //#region src/types/transport.d.ts /** * Transport interface for EventMsgV3 protocol. * Transport handles the physical connection and device addressing. */ interface Transport extends EventEmitter { /** * Get the local device address (who I am) * @returns Device address (0-255) */ getLocalAddress(): number; /** * Get the local group address (what group I'm in) * @returns Group address (0-255) */ getGroupAddress(): number; /** * Connect to the transport layer * @throws {TransportError} If connection fails */ connect(): Promise<void>; /** * Disconnect from the transport layer * @throws {TransportError} If disconnection fails */ disconnect(): Promise<void>; /** * Check if transport is currently connected * @returns True if connected, false otherwise */ isConnected(): boolean; /** * Send raw binary data through the transport * @param data Binary data to send * @throws {TransportError} If send fails */ send(data: Uint8Array): Promise<void>; } /** * Base configuration interface for transports. * Specific transports extend this with their own options. */ interface TransportConfig { /** * Local device address (who I am) * Valid range: 0-255 */ localAddress: number; /** * Local group address (what group I'm in) * Valid range: 0-255 */ groupAddress: number; } //#endregion //#region src/types/base-transport.d.ts /** * Connection state for transport implementations */ type ConnectionState = 'disconnected' | 'connecting' | 'connected'; /** * 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 */ declare abstract class BaseTransport<TConfig extends TransportConfig = TransportConfig> extends EventEmitter implements Transport { protected readonly config: TConfig; protected connectionState: ConnectionState; private connectStartTime; constructor(config: TConfig); /** * Transport-specific connection implementation. * Should establish the physical/logical connection. * @throws {Error} Transport-specific connection errors */ protected abstract doConnect(): Promise<void>; /** * Transport-specific disconnection implementation. * Should clean up and close the connection. * @throws {Error} Transport-specific disconnection errors */ protected abstract doDisconnect(): Promise<void>; /** * Transport-specific send implementation. * Should transmit the data through the transport medium. * @param data - Binary data to send * @throws {Error} Transport-specific send errors */ protected abstract doSend(data: Uint8Array): Promise<void>; /** * Connect to the transport layer. * Handles state management and error wrapping around doConnect(). */ connect(): Promise<void>; /** * Disconnect from the transport layer. * Handles state management and error wrapping around doDisconnect(). */ disconnect(): Promise<void>; /** * Check if transport is currently connected. */ isConnected(): boolean; /** * Send raw binary data through the transport. * Validates state and data before calling doSend(). */ send(data: Uint8Array): Promise<void>; /** * Get the local device address. */ getLocalAddress(): number; /** * Get the local group address. */ getGroupAddress(): number; /** * Validate transport configuration. * @param config - Configuration to validate * @throws {ValidationError} If configuration is invalid */ protected validateConfig(config: TConfig): void; /** * Generate error context for consistent error reporting. * @param operation - Operation being performed * @param additional - Additional context data * @returns Error context object */ protected getErrorContext(operation: string, additional?: Record<string, unknown>): Record<string, unknown>; /** * Emit a transport error event. * @param error - Error to emit */ protected emitError(error: Error): void; /** * Handle unexpected disconnection. * Should be called by transport implementations when connection is lost. */ protected handleUnexpectedDisconnect(reason?: Error): void; } //#endregion //#region src/types/logger.d.ts /** * Simple logging configuration for Consola */ interface LoggingConfig { /** * Enable/disable logging * @default true */ enabled?: boolean; /** * Consola log level (0-5) * 0=Error, 1=Warn, 2=Log, 3=Info, 4=Debug, 5=Trace * @default 3 */ level?: number; } /** * Default logging configuration */ declare const DEFAULT_LOGGING_CONFIG: LoggingConfig; /** * Log namespaces for tagging */ declare const LOG_NAMESPACES: { readonly CORE: "eventmsg:core"; readonly PROTOCOL: "eventmsg:protocol"; readonly TRANSPORT: "eventmsg:transport"; readonly TRANSPORT_WEBBLE: "eventmsg:transport:webble"; }; //#endregion //#region src/types/config.d.ts /** * Configuration for EventMsg class */ interface EventMsgConfig { /** Transport instance (handles addressing and connection) */ transport: Transport; /** Maximum message size in bytes after stuffing (default: 4096) */ maxMessageSize?: number; /** Default timeout for operations in milliseconds (default: 5000) */ messageTimeout?: number; /** Text encoding for message data (default: 'utf8') */ encoding?: 'utf8' | 'binary'; /** Logging configuration (replaces debug option) */ logging?: LoggingConfig; /** Protocol-specific options */ protocol?: ProtocolOptions; } /** * Protocol-specific configuration options */ interface ProtocolOptions { /** Enable strict message validation (default: true) */ strictValidation?: boolean; /** Maximum event name length in bytes (default: 64) */ maxEventNameLength?: number; /** Maximum event data length in bytes (default: 3048) */ maxEventDataLength?: number; } /** * Default protocol options */ declare const DEFAULT_PROTOCOL_OPTIONS: Required<ProtocolOptions>; /** * Default configuration values */ declare const DEFAULT_CONFIG: Required<Omit<EventMsgConfig, 'transport'>>; //#endregion //#region src/types/message.d.ts /** * Message header structure matching the C implementation exactly. * Total size: 7 bytes before byte stuffing */ interface MessageHeader { /** Source device address (0-255) */ senderId: number; /** Target device address (0-255, 0xFF = broadcast) */ receiverId: number; /** Source group address (0-255) */ senderGroupId: number; /** Target group address (0-255, 0xFF = broadcast group) */ receiverGroupId: number; /** Control flags (typically 0x00) */ flags: number; /** Message sequence number (0-65535, big-endian) */ messageId: number; } /** * Options for sending messages */ interface SendOptions { /** Target device address - REQUIRED for explicit targeting */ receiverId: number; /** Target group address (default: 0x00) */ receiverGroupId?: number; /** Control flags (default: 0x00) */ flags?: number; /** Message timeout in milliseconds (overrides config default) */ timeout?: number; } /** * Metadata provided with received messages */ interface MessageMetadata { /** Source device address */ senderId: number; /** Source group address */ senderGroupId: number; /** Target device address */ receiverId: number; /** Target group address */ receiverGroupId: number; /** Message sequence number */ messageId: number; /** Control flags */ flags: number; /** Timestamp when message was received */ timestamp: Date; } /** * Complete message result with data and metadata */ interface MessageResult<TData = unknown> { /** The decoded message data */ data: TData; /** Message metadata (addressing, timing, etc.) */ metadata: MessageMetadata; } /** * Options for waitFor method */ interface WaitForOptions { /** Timeout in milliseconds (default: config.messageTimeout) */ timeout?: number; /** Filter function to match specific messages */ filter?: (metadata: MessageMetadata) => boolean; } /** * Internal pending wait structure */ //#endregion //#region src/types/events.d.ts /** * Event handler function type for type-safe event handling */ type MessageHandler<TData = unknown> = (data: TData, metadata: MessageMetadata) => void; /** * Internal event names used by EventMsg */ declare const INTERNAL_EVENTS: { /** Emitted when a message is received */ readonly MESSAGE: "message"; /** Emitted when connection is established */ readonly CONNECT: "connect"; /** Emitted when connection is lost */ readonly DISCONNECT: "disconnect"; /** Emitted on errors */ readonly ERROR: "error"; /** Emitted before sending a message (debug) */ readonly SEND: "send"; }; /** * Message information passed to onMessage handlers */ /** * Connection statistics for monitoring */ interface ConnectionStats { /** Total messages sent */ messagesSent: number; /** Total messages received */ messagesReceived: number; /** Connection uptime in milliseconds */ uptime: number; /** Current connection state */ connected: boolean; /** Last error timestamp */ lastError?: Date; /** Average message round-trip time in milliseconds */ averageRtt?: number; } //#endregion //#region src/event-msg.d.ts /** * EventMsg - Main class for EventMsgV3 protocol communication * * Provides type-safe messaging with per-method generics, async event handling, * and transport abstraction. */ declare class EventMsg extends EventEmitter { private readonly transport; private readonly protocol; private readonly logger; private messageCounter; private readonly config; private connectionState; private connectTime; private pendingWaits; private stats; private messageHandlers; constructor(config: EventMsgConfig); /** * Connect to the transport layer */ connect(): Promise<void>; /** * Disconnect from the transport layer */ disconnect(): Promise<void>; /** * Check if currently connected */ isConnected(): boolean; /** * Send a message with explicit receiver targeting */ send<TData = unknown>(event: string, data: TData, options: SendOptions): Promise<void>; /** * Send binary data with explicit receiver targeting */ sendBinary(event: string, data: Uint8Array, options: SendOptions): Promise<void>; /** * Register a type-safe event handler */ on<TData = unknown>(event: string | symbol, handler: MessageHandler<TData>): this; /** * Register a one-time type-safe event handler */ once<TData = unknown>(event: string | symbol, handler: MessageHandler<TData>): this; /** * Remove a type-safe event handler */ off<TData = unknown>(event: string | symbol, handler?: MessageHandler<TData>): this; /** * Register a handler for all incoming user messages * @param handler - Function called for every incoming message * @returns this instance for chaining * @example * ```typescript * eventMsg.onMessage((eventName, data, metadata) => { * console.log(`Received ${eventName} from device ${metadata.senderId}`); * }); * ``` */ onMessage(handler: (eventName: string, data: unknown, metadata: MessageMetadata) => void): this; /** * Register a handler for a specific message event type * @param eventName - The specific event name to listen for * @param handler - Function called when the specific event is received * @returns this instance for chaining * @example * ```typescript * eventMsg.onMessage('ping', (data, metadata) => { * console.log(`Ping from device ${metadata.senderId}: ${data}`); * }); * ``` */ onMessage<TData = unknown>(eventName: string, handler: (data: TData, metadata: MessageMetadata) => void): 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?: string): this; /** * Track message handlers for proper cleanup */ private trackMessageHandler; /** * Wait for a single event occurrence with timeout */ waitFor<TData = unknown>(event: string, options?: WaitForOptions): Promise<MessageResult<TData>>; /** * Get local device address from transport */ getLocalAddress(): number; /** * Get local group address from transport */ getGroupAddress(): number; /** * Get connection statistics */ getStats(): ConnectionStats; /** * Create message header from send options */ private createHeader; /** * Validate send options */ private validateSendOptions; /** * Setup transport event handlers */ private setupTransportHandlers; /** * Handle incoming transport data */ private handleIncomingData; /** * Handle transport errors */ private handleTransportError; /** * Handle transport disconnection */ private handleTransportDisconnect; /** * Add a pending wait for an event */ private addPendingWait; /** * Remove a pending wait */ private removePendingWait; /** * Resolve pending waits for an event */ private resolvePendingWaits; /** * Clear all pending waits (on disconnect) */ private clearAllPendingWaits; } //#endregion //#region src/internal/byte-stuffing.d.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 */ declare const CONTROL_CHARS: { readonly SOH: 1; readonly STX: 2; readonly EOT: 4; readonly US: 31; readonly ESC: 27; }; /** * 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 */ declare function stuff(data: Uint8Array): Uint8Array; /** * 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 */ declare function unstuff(data: Uint8Array): Uint8Array; /** * 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 */ declare function getMaxStuffedSize(originalSize: number): number; /** * Calculate the minimum possible size after unstuffing * In best case, no bytes are stuffed * * @param stuffedSize Stuffed data size * @returns Minimum size after unstuffing */ declare function getMinUnstuffedSize(stuffedSize: number): number; /** * Test if data contains any control characters that would need stuffing * * @param data Data to test * @returns True if data contains control characters */ declare function needsStuffing(data: Uint8Array): boolean; /** * Validate that stuffed data has proper escape sequences * * @param data Stuffed data to validate * @returns True if data has valid stuffing */ declare function isValidStuffing(data: Uint8Array): boolean; //#endregion //#region src/internal/logger.d.ts declare let globalConsola: ReturnType<typeof createConsola>; /** * Configure global logging */ /** * Get logger for a component */ declare function getLogger(namespace: keyof typeof LOG_NAMESPACES): ReturnType<typeof globalConsola.withTag>; /** * Simple hex dump for debugging */ declare function hexDump(data: Uint8Array, maxBytes?: number): string; //#endregion //#region src/protocol.d.ts /** * Decoded message structure */ interface DecodedMessage { header: MessageHeader; eventName: string; eventData: string; } /** * EventMsgV3 Protocol Implementation * * Handles encoding and decoding of EventMsgV3 binary protocol messages. * Message format: [SOH][StuffedHeader][STX][StuffedEventName][US][StuffedEventData][EOT] */ declare class Protocol { private readonly options; private readonly encoding; private readonly logger; constructor(config: EventMsgConfig); /** * 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: MessageHeader, eventName: string, eventData: string): Uint8Array; /** * 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: MessageHeader, eventName: string, eventData: Uint8Array): Uint8Array; /** * 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: Uint8Array): DecodedMessage; /** * Find a control character in data, skipping escaped instances */ private findControlChar; /** * Serialize message header to 7 bytes (big-endian messageId) */ private serializeHeader; /** * Deserialize 7 bytes to message header (big-endian messageId) */ private deserializeHeader; /** * Convert string to bytes using configured encoding */ private stringToBytes; /** * Convert bytes to string using configured encoding */ private bytesToString; /** * Validate message header fields */ private validateHeader; /** * Validate event name length */ private validateEventName; /** * Validate event data length */ private validateEventData; } //#endregion export { AddressValidationError, BaseTransport, CONTROL_CHARS, ConnectionError, type ConnectionStats, ConnectionTimeoutError, DEFAULT_CONFIG, DEFAULT_LOGGING_CONFIG, DEFAULT_PROTOCOL_OPTIONS, type DecodedMessage, DecodingError, DisconnectionError, EncodingError, EventMsg, type EventMsgConfig, EventMsgError, INTERNAL_EVENTS, InvalidMessageError, LOG_NAMESPACES, type LoggingConfig, type MessageHandler, type MessageHeader, type MessageMetadata, type MessageResult, MessageSizeError, Protocol, ProtocolError, type ProtocolOptions, ValidationError as ProtocolValidationError, SendError, type SendOptions, SendTimeoutError, TimeoutError, type Transport, type TransportConfig, TransportError, ValidationError$1 as ValidationError, type WaitForOptions, WaitForTimeoutError, getLogger, getMaxStuffedSize, getMinUnstuffedSize, hexDump, isValidStuffing, needsStuffing, stuff, unstuff }; //# sourceMappingURL=index.d.ts.map