@eventmsg/core
Version:
EventMsgV3 TypeScript library - Core protocol implementation with transport abstraction
857 lines (854 loc) • 24.8 kB
TypeScript
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