@nolag/chat

Multi-room chat with presence, typing indicators, streamed messages, and user mapping.

Overview

@nolag/chat is a high-level SDK that turns any application into a fully-featured chat system. It handles room management, user presence, typing indicators, streamed (token-by-token) messages, and unread tracking, all on top of @nolag/js-sdk. Your app owns one core NoLag client and injects it into NoLagChat; the wrapper attaches its chat behaviour to that connection. You create a NoLagChat instance, wait for it to be ready, join rooms, and start sending messages within minutes.

Key Features

  • Multi-room chat with isolated presence per room
  • Typing indicators with automatic timeout
  • Streamed messages that grow live on every screen, for AI responses
  • User mapping to attach names, avatars, and metadata to each actor
  • Unread message tracking with per-room badge counts
  • Automatic reconnection with subscriptions and presence restored

How It Works

NoLagChat attaches to an injected @nolag/js-sdk client and manages a lobby for global user presence. When you call joinRoom(), it returns a ChatRoom instance that subscribes to three topics: messages for finished chat messages, _stream for in-progress streamed message chunks, and _typing for typing signals. A MessageStore inside each room accumulates messages, while a PresenceManager tracks who is currently online. The app owns the socket lifecycle; the wrapper never opens or closes it.

TopicPurpose
messagesFinished chat messages: text, data, sender info
_streamLive chunks of a streamed message (start, delta, abort)
_typingTyping start/stop signals

A fresh subscribe or an ordinary reconnect never replays history; see Replay for the one case where the broker replays messages.

Before you start. Create an app from the nolag-chat-sdk blueprint. That seeds the rooms this SDK expects (general, random, help, dev) and the online lobby, and returns an app slug with a random suffix. That slug is the appName you pass to the wrapper. You can also do this in the portal: Apps, New App, pick the blueprint.

import { NoLagApi } from "@nolag/js-sdk";

const api = new NoLagApi(process.env.NOLAG_API_KEY); // nlg_live_...
const app = await api.apps.create({ name: "My App", blueprintId: "nolag-chat-sdk" });
console.log(app.slug); // e.g. "my-app-a3f9": this is your appName

const actor = await api.actors.create({ name: "web-client", actorType: "user" });
console.log(actor.accessToken); // shown once; keep it

Installation

npm install @nolag/chat @nolag/js-sdk

Shared connection. One core NoLag client can back several wrapper SDKs at once, for example chat, notify, and a dashboard on a single socket, as long as each wrapper uses a distinct appName. Each wrapper attaches its handlers on construction and releases them with detach(), and never touches the socket itself. Your app owns connect() and disconnect().

Quick Start

import { NoLag } from '@nolag/js-sdk'
import { NoLagChat } from '@nolag/chat'

// The app owns one core client. In a browser, pass a token provider so the
// SDK can mint fresh short-lived client tokens from your backend.
const client = NoLag(async () => (await (await fetch('/api/nolag-token')).json()).token)

// Inject the client into the chat wrapper
const chat = new NoLagChat({
  client,
  appName: APP_SLUG, // the slug returned when you created the app
  username: 'Alice',
  avatar: '/img/alice.png',
})

await client.connect()   // the app owns the connection
await chat.ready()       // wrapper setup complete (identity, presence, rooms)

// Join a seeded room
const room = chat.joinRoom('general')

// Send a message. Publishers never receive their own messages back, so the
// wrapper adds it to the local store for you and emits `messageSent`.
room.sendMessage('Hello everyone!')

// Listen for messages from other users
room.on('message', (msg) => {
  console.log(`[${msg.username}]: ${msg.text}`)
  console.log('Sent at:', new Date(msg.timestamp))
})

// Listen for typing indicators
room.on('typing', ({ users }) => {
  console.log('Typing:', users.map((u) => u.username).join(', '))
})

// Trigger typing events
room.startTyping()
// ... user stops typing
room.stopTyping()

// Get online users in this room
const users = room.getUsers()
console.log('Online:', users.length)

// Teardown: the wrapper releases its handlers and topics; the app closes the
// socket (never the other way around).
chat.detach()
client.disconnect()

API Reference

NoLagChat

The main class. Attaches to the injected core client, manages global user presence, and the room lifecycle.

Constructor Options

OptionTypeDescription
clientNoLagSocketRequired. The injected core NoLag client the app owns and connects.
usernamestringRequired. Display name for this user.
avatarstringOptional avatar URL.
metadataRecord<string, unknown>Optional custom user data attached to presence.
appNamestringThe app slug returned when you created the app (default 'chat', which only works if an app with exactly that slug exists).
roomsstring[]Rooms to subscribe to once the wrapper is ready.
typingTimeoutnumberMs before a typing indicator auto-clears (default 3000).
maxMessageCachenumberMax messages kept in memory per room (default 500).
debugbooleanEnable wrapper debug logging (default false).
MethodReturnsDescription
ready()Promise<void>Resolves once wrapper setup completed (identity, lobby, configured rooms)
detach()voidRelease this wrapper's handlers and topics; terminal, never closes the socket
joinRoom(name, opts?)ChatRoomJoin a chat room; throws if called before ready() resolves
leaveRoom(name)voidLeave a room and unsubscribe from its topics
getRooms()ChatRoom[]All joined rooms
getOnlineUsers()ChatUser[]All users currently online in the lobby
setStatus(status)voidUpdate your own presence status ('online', 'away', 'busy', 'offline')
updateProfile(profile)voidUpdate display name, avatar, or metadata broadcast to peers

Events: NoLagChat

EventPayloadDescription
connectednoneWrapper setup completed on a fresh connection
disconnectedreason: stringConnection closed
reconnectingnoneThe client is attempting to reconnect
reconnectednoneReconnection successful; rooms are restored automatically
errorerror: ErrorUnrecoverable error occurred
userOnlineuser: ChatUserA user has come online in the lobby
userOfflineuser: ChatUserA user has gone offline
userUpdateduser: ChatUserA user updated their profile or status

ChatRoom

Returned by joinRoom(). Scoped to a single room; handles messaging, typing, and per-room presence.

MethodReturnsDescription
sendMessage(text, opts?)ChatMessagePublish a chat message to the room; returns the optimistic local copy
startStream(opts?)MessageStreamBegin a streamed message; append(text) tokens, then complete() or abort()
streamMessage(source, opts?)Promise<ChatMessage>Stream every chunk of an async iterable (an LLM response) and finalise it
getMessages()ChatMessage[]All messages currently in the local store, in timestamp order
startTyping()voidBroadcast a typing-start signal to other room members
stopTyping()voidBroadcast a typing-stop signal
getUsers()ChatUser[]Remote users currently present in this room
getUser(userId)ChatUser | undefinedOne remote user by userId
markRead()voidReset this room's unread count to zero
setFilters(values)voidReplace the room's subscription filters; see Filters

Events: ChatRoom

EventPayloadDescription
messageChatMessageIncoming message from another user
messageSentChatMessageYour own message was added to the local store and published
userJoinedChatUserA user joined this room
userLeftChatUserA user left this room
typing{ users: ChatUser[] }The set of users currently typing changed
streamStartChatMessageA streamed message started (status 'streaming')
streamChunk{ message: ChatMessage, delta: string }A streamed message grew; message.text is already updated
streamEndChatMessageA streamed message finished
streamAbort{ message: ChatMessage, error?: string }A streamed message was cancelled
unreadChanged{ room: string, count: number }The unread message count for this room changed