@nolag/collab
Real-time collaboration with operations, cursor tracking, and idle awareness.
Overview
Add real-time collaboration to any editor or canvas. Operations (insert, delete, replace, format, or custom domain types) are broadcast to all document participants and stored for 24-hour replay. Cursor positions are synchronised via a throttled ephemeral channel so other participants see where each user is working without generating durable storage traffic. Users are automatically marked idle by the SDK after a configurable period of no cursor activity, and marked editing again on the next cursor update. Each joined document is an independent collaboration session; join multiple documents for multi-tab or split-pane editors. Your app owns one core NoLag client and injects it into NoLagCollab; the wrapper attaches its behaviour to that connection.
Key Features
- Five built-in operation types: insert, delete, replace, format, and custom
- 24-hour operation history with replay on reconnect
- Throttled ephemeral cursor sync with configurable interval
- Automatic idle detection after configurable inactivity timeout
- User online/offline presence via lobby
- Per-document user join/leave events
- Multiple documents per connection
How It Works
NoLagCollab attaches to an injected @nolag/js-sdk client and maintains a lobby for user presence. Calling joinDocument(name) returns a CollabDocument that subscribes to two topics: operations for durable operation history and _cursors for ephemeral cursor position updates. The SDK throttles outbound cursor messages to cursorThrottle (default 50 ms) and starts an idle timer that resets on every cursor update. When the timer fires the SDK emits awarenessChanged locally and broadcasts the idle status to other participants. The app owns the socket lifecycle; the wrapper never opens or closes it.
| Topic | Purpose | Replay |
|---|---|---|
operations | Document operations: insert, delete, replace, format, custom | 24 hours |
_cursors | Real-time cursor positions and awareness status (not persisted) | Ephemeral |
Installation
npm install @nolag/collab @nolag/js-sdkOne core NoLag client can back several wrapper SDKs at once, for example collaboration, chat, and notify 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 { NoLagCollab } from '@nolag/collab'
// 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 collab wrapper
const collab = new NoLagCollab({
client,
username: 'Alice',
idleTimeout: 30_000, // Mark user idle after 30 s without cursor activity
cursorThrottle: 50, // Throttle cursor updates (default 50 ms)
})
await client.connect() // the app owns the connection
await collab.ready() // wrapper setup complete
// Join a document (each document is a separate collaboration session)
const doc = await collab.joinDocument('doc-proposal-q3')
// ── Operations ────────────────────────────────────────────────────────────────
// Send an insert operation
doc.sendOperation('insert', {
position: 42,
content: 'Hello, world!',
})
// Send a delete operation
doc.sendOperation('delete', {
position: 42,
length: 13,
})
// Send a format operation
doc.sendOperation('format', {
position: 10,
length: 5,
attributes: { bold: true },
})
// Send a custom operation (any domain-specific action)
doc.sendOperation('custom', {
type: 'highlight',
color: '#ffcc00',
range: { start: 0, end: 20 },
})
// Retrieve stored operations (replayed from last 24 hours on reconnect)
const ops = await doc.getOperations()
console.log(`${ops.length} operations in history`)
// React to operations from other users
doc.on('operation', ({ userId, type, opts, timestamp }) => {
console.log(`User ${userId} sent ${type} op`, opts)
})
// ── Cursors ───────────────────────────────────────────────────────────────────
// Broadcast cursor position (throttled to cursorThrottle)
doc.updateCursor({ position: 42, line: 3, column: 8 })
// Read all cursors from local cache
const cursors = doc.getCursors()
console.log('Active cursors:', cursors.length)
// React to cursor moves from other users
doc.on('cursorMoved', ({ userId, position, line, column }) => {
renderCursor(userId, { line, column })
})
// ── Awareness ─────────────────────────────────────────────────────────────────
// Update own status
doc.setStatus('editing') // 'editing' | 'idle' | 'away'
// React to status changes (idle is set automatically by the SDK)
doc.on('awarenessChanged', ({ userId, status }) => {
console.log(`User ${userId} is now ${status}`)
})
// ── Presence ──────────────────────────────────────────────────────────────────
collab.on('userOnline', ({ userId }) => console.log('Joined lobby:', userId))
collab.on('userOffline', ({ userId }) => console.log('Left lobby:', userId))
doc.on('userJoined', ({ userId }) => console.log('Joined document:', userId))
doc.on('userLeft', ({ userId }) => console.log('Left document:', userId))
// ── Teardown ──────────────────────────────────────────────────────────────────
// The wrapper releases its handlers; the app closes the socket.
collab.detach()
client.disconnect()API Reference
NoLagCollab
Constructor Options
| Option | Type | Description |
|---|---|---|
client | NoLagSocket | Required. The injected core NoLag client the app owns and connects. |
username | string | Required. Display name for the local user. |
avatar | string | Optional avatar URL. |
color | string | Optional cursor/highlight colour. |
metadata | Record<string, unknown> | Optional custom data attached to user presence. |
appName | string | NoLag app for topic prefixes (default 'collab'). |
documents | string[] | Documents to auto-join once the wrapper is ready. |
maxOperationCache | number | Max operations cached per document (default 1000). |
idleTimeout | number | Ms of inactivity before a user is marked idle (default 60000). |
cursorThrottle | number | Minimum ms between cursor broadcasts (default 50). |
debug | boolean | Enable wrapper debug logging (default false). |
| Method | Returns | Description |
|---|---|---|
ready() | Promise<void> | Resolves once wrapper setup completed. |
detach() | void | Release this wrapper's handlers and topics; terminal, never closes the socket. |
joinDocument(name) | Promise<CollabDocument> | Subscribe to a collaboration document. Returns the document instance. |
leaveDocument(name) | Promise<void> | Unsubscribe from a document and release its resources. |
NoLagCollab Events
| Event | Payload | Description |
|---|---|---|
connected | none | WebSocket connection established. |
disconnected | reason: string | Connection closed. |
reconnected | none | Connection restored; operation history replay begins automatically. |
error | error: Error | A transport or protocol error occurred. |
userOnline | { userId: string } | A user joined the lobby. |
userOffline | { userId: string } | A user left the lobby. |
CollabDocument
| Method | Returns | Description |
|---|---|---|
sendOperation(type, opts?) | void | Broadcast an operation. type is 'insert' | 'delete' | 'replace' | 'format' | 'custom'. opts is type-specific. |
getOperations() | Promise<Operation[]> | Fetch the stored operation history for this document (up to 24 hours). |
updateCursor(opts) | void | Broadcast current cursor position. Automatically throttled. Resets the idle timer. |
getCursors() | Cursor[] | Return cursor positions for all active users from the local cache. |
setStatus(status) | void | Manually set awareness status: 'editing' | 'idle' | 'away'. Broadcast to all participants. |
CollabDocument Events
| Event | Payload | Description |
|---|---|---|
operation | { userId, type, opts?, timestamp } | An operation was broadcast by another user. |
cursorMoved | { userId, position, line?, column?, timestamp } | A user's cursor position changed. Delivered via the ephemeral channel. |
userJoined | { userId: string } | A user joined this document. |
userLeft | { userId: string } | A user left this document. |
awarenessChanged | { userId, status: 'editing' | 'idle' | 'away' } | A user's awareness status changed. Fired locally when the idle timer elapses. |
replayStart | { count: number } | Operation history replay has begun. |
replayEnd | { replayed: number } | Operation history replay has completed. |