Transport API
ITransport abstracts realtime communication from ChatEngine.
Interface
export interface ITransport<
TInbound extends TransportEvent = TransportEvent,
TOutbound extends TransportEvent = TransportEvent,
> {
connect(): Promise<void>;
disconnect(): Promise<void>;
send(event: TOutbound): Promise<void>;
onMessage(handler: (event: TInbound) => void): Unsubscribe;
onStateChange(handler: (state: ConnectionState) => void): Unsubscribe;
getState(): ConnectionState;
}TransportEvent is a discriminated generic built from TransportEventMap.
Most adapters narrow this to the specific event types they actually handle,
for example TransportEvent<'message'>.
Custom WebSocket Transport Example
import type { ConnectionState, ITransport, TransportEvent, Unsubscribe } from '@kaira/chat-core';
type MessageTransportEvent = TransportEvent<'message'>;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isMessageTransportEvent(value: unknown): value is MessageTransportEvent {
return (
isRecord(value) &&
value['type'] === 'message' &&
typeof value['timestamp'] === 'number' &&
isRecord(value['payload']) &&
typeof value['payload']['id'] === 'string' &&
typeof value['payload']['conversationId'] === 'string'
);
}
export class WebSocketTransport implements ITransport<
MessageTransportEvent,
MessageTransportEvent
> {
private socket: WebSocket | undefined;
private state: ConnectionState = 'disconnected';
private readonly messageHandlers = new Set<(event: MessageTransportEvent) => void>();
private readonly stateHandlers = new Set<(state: ConnectionState) => void>();
constructor(private readonly url: string) {}
async connect(): Promise<void> {
if (this.state !== 'disconnected') return;
this.setState('connecting');
this.socket = new WebSocket(this.url);
await new Promise<void>((resolve, reject) => {
if (!this.socket) return reject(new Error('Socket unavailable'));
this.socket.onopen = () => {
this.setState('connected');
resolve();
};
this.socket.onerror = () => reject(new Error('WebSocket connection failed'));
this.socket.onmessage = (event) => {
const payload = JSON.parse(event.data) as unknown;
if (!isMessageTransportEvent(payload)) {
return;
}
for (const handler of this.messageHandlers) handler(payload);
};
this.socket.onclose = () => this.setState('reconnecting');
});
}
async disconnect(): Promise<void> {
if (!this.socket) return;
this.setState('disconnecting');
this.socket.close();
this.socket = undefined;
this.setState('disconnected');
}
async send(event: MessageTransportEvent): Promise<void> {
if (!this.socket || this.state !== 'connected') return;
this.socket.send(JSON.stringify(event));
}
onMessage(handler: (event: MessageTransportEvent) => void): Unsubscribe {
this.messageHandlers.add(handler);
return () => this.messageHandlers.delete(handler);
}
onStateChange(handler: (state: ConnectionState) => void): Unsubscribe {
this.stateHandlers.add(handler);
return () => this.stateHandlers.delete(handler);
}
getState(): ConnectionState {
return this.state;
}
private setState(next: ConnectionState): void {
this.state = next;
for (const handler of this.stateHandlers) handler(next);
}
}Polling Transport
Use the provided adapter:
import { PollingTransport } from '@kaira/chat-transport-polling';
const transport = new PollingTransport({
intervalMs: 2000,
pollImmediately: true,
poll: async () => [],
send: async () => {},
onPollError: (error) => {
console.error('poll failed', error);
},
});Notes:
intervalMsdefaults to2000.pollImmediatelydefaults totrue.- Failed polls transition the transport to
reconnectingand reschedule with capped exponential backoff plus jitter. sendis optional; if omitted, outbound sends are a no-op.onPollErroris optional and receives poll exceptions.