---
title: "Collab SDK"
description: "Real-time collaboration with operations, cursor tracking, and idle awareness for editors and canvases."
---

# @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

```bash [Terminal]
npm install @nolag/collab @nolag/js-sdk
```

One 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

```typescript [TypeScript]
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. |
