@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.
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 wraps the @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 cursorThrottleMs (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.
| 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-sdkQuick Start
import { NoLagCollab } from '@nolag/collab'
const collab = new NoLagCollab('your-access-token', {
idleTimeoutMs: 30_000, // Mark user idle after 30 s without cursor activity
cursorThrottleMs: 50, // Throttle cursor updates (default 50 ms)
})
await collab.connect()
// 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 cursorThrottleMs)
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))API Reference
NoLagCollab
| Method | Returns | Description |
|---|---|---|
connect() | Promise<void> | Establish the WebSocket connection and join the lobby. |
disconnect() | void | Close the connection and leave the lobby. |
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. |