---
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.

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

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

## Quick Start

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