Docs menu

@dotwire/client

@dotwire/client is the member-facing SDK for browsers and JavaScript runtimes. It connects to the dotwire hub over WebSockets, subscribes to rooms, sends messages, loads history and participants, and handles typing and presence. It requires Node.js 20 or later when you run it outside a browser.

For React, use @dotwire/react, which wraps this package.

Install

@dotwire/client on npm

npm install @dotwire/client

Quickstart

import { DotwireClient } from '@dotwire/client';

const client = new DotwireClient({
  baseUrl: 'https://chat.yourdomain.com',
  tokenUrl: '/api/auth/token', // GET, credentials included, expects `{ token }`
  automaticReconnect: true,
  autoStopTypingMs: 3000,
  typingThrottleMs: 1000
});

const room = client.room('a0000000-0000-0000-0000-000000000001');

await room.subscribe();

const unsubMessage = room.onMessage((message) => {
  renderMessage(message);
});

const ack = await room.sendMessage('Hello, world!');

const history = await room.getHistory({ limit: 50 });

const participantIds = await room.getParticipants();

unsubMessage();
room.dispose();
await client.disconnect();

renderMessage stands for your own UI code. tokenUrl points at the token endpoint your host exposes. See Integrate a host app.

Options

Pass these to new DotwireClient(options). You must set either tokenUrl or getAccessToken.

OptionDefaultDescription
baseUrlRequireddotwire's base URL.
tokenUrl—Host token endpoint. Called with GET and credentials: 'include'; must return { token }. The SDK caches and refreshes the token.
getAccessToken—Alternative to tokenUrl. Returns a token; you own caching and refresh.
tokenRefreshMarginMs30000How long before expiry a cached token is refreshed.
sendUrl—A host route for sends. When set, sendMessage posts { roomId, content } there with credentials: 'include' and no bearer header. See Moderation webhooks.
hubPath/hub/roomsHub path on baseUrl.
transport'websockets''websockets' skips negotiation. 'negotiate' uses SignalR negotiation.
serverTimeoutMs90000SignalR server timeout.
keepAliveIntervalMs15000SignalR client keep-alive interval.
automaticReconnecttrueReconnect automatically and resubscribe rooms.
gapFillOnReconnecttrueAfter a reconnect, fetch missed messages by sequence.
gapFillPageSize100Page size for gap-fill requests.
requestTimeoutMs15000Timeout for HTTP requests.
autoStopTypingMs3000How long after the last notifyTyping() the SDK sends "stopped typing".
typingThrottleMs1000Minimum interval between typing notifications sent to the server.
logLevel'none''none', 'error', 'warning', 'information', or 'debug'.

Room handles

Everything room-specific goes through client.room(roomId), which returns a DotwireRoomHandle.

const room = client.room('room-uuid');

// Connect & join group
await room.subscribe();

// Leave room
await room.unsubscribe();

// Clean up all room listeners and auto-unsubscribe
room.dispose();

subscribe({ history: n }) also fetches the latest n messages, delivers each one through onMessage, and returns that page.

History

// Fetch latest 50 messages
const latest = await room.getHistory({ limit: 50 });

// Paginate older messages before a sequence number
const older = await room.getHistory({
  beforeSeq: latest.messages[0].seq,
  limit: 50
});

// Replay missed messages after a reconnection
const missed = await room.getHistory({
  afterSeq: lastKnownSeq,
  limit: 100
});

You don't need to replay manually after an automatic reconnect: with gapFillOnReconnect on, the SDK pages getHistory({ afterSeq }) itself and delivers each missed message through onMessage with replayed: true. The SDK also deduplicates by seq, using a bounded set of the last 1,000 sequence numbers per room, so each message fires onMessage once.

Typing

function onUserKeystroke() {
  room.notifyTyping();
}

function onUserSubmit() {
  room.stopTyping();
}

const unsubTyping = room.onTypingWithExpiry((event) => {
  if (event.isTyping) {
    showTypingIndicator(event.userId);
  } else {
    hideTypingIndicator(event.userId);
  }
}, 3500);

notifyTyping() throttles what it sends and stops automatically after a period of inactivity. onTypingWithExpiry treats a user as stopped if no update arrives within the given time.

Presence

const unsubPresence = room.onPresence((delta) => {
  console.log('Joined:', delta.joined);
  console.log('Left:', delta.left);
});

Presence arrives as coalesced deltas. There is no snapshot, so you see only changes after you subscribe. For current membership, use getParticipants(). See Participants and presence.

System messages

room.onSystemMessage((message) => {
  showSystemBanner(message.content);
});

room.onUserMessage((message) => {
  appendUserChatMessage(message);
});

A message counts as a system message when its sender id is system, or starts with system: or bot:.

API

DotwireClient

MemberReturnsDescription
room(roomId)DotwireRoomHandleA handle for one room.
connect()Promise<void>Opens the hub connection.
disconnect()Promise<void>Closes the connection and leaves all rooms.
getConnectionState()ConnectionState'disconnected', 'connecting', 'connected', 'reconnecting', or 'disconnecting'.
onMessage(handler)() => voidMessages from every subscribed room.
onSystemMessage(handler), onUserMessage(handler)() => voidSystem messages only, or user messages only.
onMessageRetracted(handler)() => voidRedacted messages.
onMembershipRevoked(handler)() => voidFires when the user is removed from a room. The room is unsubscribed automatically.
onTyping(handler), onPresence(handler)() => voidTyping and presence events from every subscribed room.
onStateChange(handler)() => voidConnection state changes.
dispose()voidDisposes every room handle and disconnects.

Every on… method returns a function that removes the listener.

DotwireRoomHandle

MemberReturnsDescription
subscribe(options?)Promise<HistoryPage | undefined>Joins the room. With { history: n }, also loads and delivers the latest n messages.
unsubscribe()Promise<void>Leaves the room.
sendMessage(content)Promise<SendAck>Sends a message. Resolves with { seq, time } once JetStream accepts it.
getHistory(options)Promise<HistoryPage>One page of history: { messages, hasMore }. Takes afterSeq, beforeSeq, limit, and timeoutMs.
getParticipants(timeoutMs?)Promise<string[]>Member user ids, sorted.
notifyTyping()Promise<void>Throttled typing signal with automatic stop.
startTyping(), stopTyping()Promise<void>Explicit typing state.
onMessage, onSystemMessage, onUserMessage, onMessageRetracted, onMembershipRevoked, onTyping, onPresence() => voidRoom-scoped versions of the client listeners.
onTypingWithExpiry(handler, ms)() => voidTyping listener that resets a user after ms without updates.
release()voidCancels timers and removes listeners without leaving the room, for handles that share a subscription.
dispose()voidrelease(), then leaves the room.

Message shape

interface RoomMessage {
  roomId: string;
  seq: number;
  senderId: string;
  time: string;
  content: string;
  replayed?: boolean;
}

replayed is true only for messages delivered by reconnect gap-fill.

Errors

Failed requests reject with a DotwireClientError with status, kind, and optional code, reason, and retryAfterMs. kind is one of BadRequest, Unauthorized, Forbidden, NotFound, PresendRejected, RateLimited, Unavailable, or Unknown, the same kinds the host SDKs use. code and reason come from the error body.

Related