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:

  • intervalMs defaults to 2000.
  • pollImmediately defaults to true.
  • Failed polls transition the transport to reconnecting and reschedule with capped exponential backoff plus jitter.
  • send is optional; if omitted, outbound sends are a no-op.
  • onPollError is optional and receives poll exceptions.