@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.
| Topic | Purpose |
|---|---|
messages | Finished chat messages: text, data, sender info |
_stream | Live chunks of a streamed message (start, delta, abort) |
_typing | Typing 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 itInstallation
npm install @nolag/chat @nolag/js-sdkShared 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
| Option | Type | Description |
|---|---|---|
client | NoLagSocket | Required. The injected core NoLag client the app owns and connects. |
username | string | Required. Display name for this user. |
avatar | string | Optional avatar URL. |
metadata | Record<string, unknown> | Optional custom user data attached to presence. |
appName | string | The app slug returned when you created the app (default 'chat', which only works if an app with exactly that slug exists). |
rooms | string[] | Rooms to subscribe to once the wrapper is ready. |
typingTimeout | number | Ms before a typing indicator auto-clears (default 3000). |
maxMessageCache | number | Max messages kept in memory per room (default 500). |
debug | boolean | Enable wrapper debug logging (default false). |
| Method | Returns | Description |
|---|---|---|
ready() | Promise<void> | Resolves once wrapper setup completed (identity, lobby, configured rooms) |
detach() | void | Release this wrapper's handlers and topics; terminal, never closes the socket |
joinRoom(name, opts?) | ChatRoom | Join a chat room; throws if called before ready() resolves |
leaveRoom(name) | void | Leave a room and unsubscribe from its topics |
getRooms() | ChatRoom[] | All joined rooms |
getOnlineUsers() | ChatUser[] | All users currently online in the lobby |
setStatus(status) | void | Update your own presence status ('online', 'away', 'busy', 'offline') |
updateProfile(profile) | void | Update display name, avatar, or metadata broadcast to peers |
Events: NoLagChat
| Event | Payload | Description |
|---|---|---|
connected | none | Wrapper setup completed on a fresh connection |
disconnected | reason: string | Connection closed |
reconnecting | none | The client is attempting to reconnect |
reconnected | none | Reconnection successful; rooms are restored automatically |
error | error: Error | Unrecoverable error occurred |
userOnline | user: ChatUser | A user has come online in the lobby |
userOffline | user: ChatUser | A user has gone offline |
userUpdated | user: ChatUser | A user updated their profile or status |
ChatRoom
Returned by joinRoom(). Scoped to a single room; handles messaging, typing, and per-room presence.
| Method | Returns | Description |
|---|---|---|
sendMessage(text, opts?) | ChatMessage | Publish a chat message to the room; returns the optimistic local copy |
startStream(opts?) | MessageStream | Begin 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() | void | Broadcast a typing-start signal to other room members |
stopTyping() | void | Broadcast a typing-stop signal |
getUsers() | ChatUser[] | Remote users currently present in this room |
getUser(userId) | ChatUser | undefined | One remote user by userId |
markRead() | void | Reset this room's unread count to zero |
setFilters(values) | void | Replace the room's subscription filters; see Filters |
Events: ChatRoom
| Event | Payload | Description |
|---|---|---|
message | ChatMessage | Incoming message from another user |
messageSent | ChatMessage | Your own message was added to the local store and published |
userJoined | ChatUser | A user joined this room |
userLeft | ChatUser | A user left this room |
typing | { users: ChatUser[] } | The set of users currently typing changed |
streamStart | ChatMessage | A streamed message started (status 'streaming') |
streamChunk | { message: ChatMessage, delta: string } | A streamed message grew; message.text is already updated |
streamEnd | ChatMessage | A 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 |