Collaboration Plugin
Collaboration Plugin
Section titled “Collaboration Plugin”The CollaborationPlugin enables real-time multi-user editing using Operational Transformation (OT). Local cell edits are captured, transformed against concurrent remote operations, and broadcast to all connected clients. A companion WebSocket relay server handles operation ordering and cursor relay.
Architecture
Section titled “Architecture”Client A Server Client B───────── ────── ─────────edit cell ──→ op ─────────→ transform ──→ broadcast ──→ apply op assign rev ack sender store historyEach client maintains a local pending queue. The server assigns revision numbers and transforms operations against its history. Clients transform incoming remote operations against their pending queue before applying.
Installation
Section titled “Installation”npm install @witqq/spreadsheet @witqq/spreadsheet-pluginsPlugin Setup
Section titled “Plugin Setup”import { CollaborationPlugin, WebSocketTransport, RemoteCursorLayer } from '@witqq/spreadsheet-plugins';
const cursorLayer = new RemoteCursorLayer();const transport = new WebSocketTransport({ url: 'ws://localhost:3151', onInit: ({ clientId, color, revision, cursors }) => { console.log(`Connected as ${clientId} (${color}) at revision ${revision}`); // Initialize existing cursors for (const c of cursors) { if (c.cursor) cursorLayer.setCursor(c.clientId, { ...c, row: c.cursor.row, col: c.cursor.col }); } }, onCursor: (info) => { if (info.cursor) { cursorLayer.setCursor(info.clientId, { ...info, row: info.cursor.row, col: info.cursor.col }); } else { cursorLayer.removeCursor(info.clientId); } engine.requestRender(); }, onLeave: ({ clientId }) => { cursorLayer.removeCursor(clientId); engine.requestRender(); },});
await transport.connect();
const collab = new CollaborationPlugin({ clientId: 'unique-client-id', transport, cursorLayer, sendCursor: (cursor) => transport.sendCursor(cursor),});
engine.installPlugin(collab);CollaborationPluginConfig
Section titled “CollaborationPluginConfig”interface CollaborationPluginConfig { clientId: string; transport: OTTransport; cursorLayer?: RemoteCursorLayer; sendCursor?: (cursor: { row: number; col: number } | null) => void;}| Option | Type | Description |
|---|---|---|
clientId | string | Unique client identifier |
transport | OTTransport | Transport implementation (WebSocket or mock) |
cursorLayer | RemoteCursorLayer | Optional render layer for remote cursor display |
sendCursor | function | Callback to send cursor position updates |
OT Operation Types
Section titled “OT Operation Types”Three operation types cover all spreadsheet collaboration scenarios:
type OTOperation = SetCellValueOp | InsertRowOp | DeleteRowOp;
interface SetCellValueOp { type: 'setCellValue'; row: number; col: number; value: unknown; oldValue?: unknown;}
interface InsertRowOp { type: 'insertRow'; row: number; count: number;}
interface DeleteRowOp { type: 'deleteRow'; row: number; count: number;}Operations are wrapped in VersionedOperation for transport:
interface VersionedOperation { clientId: string; revision: number; op: OTOperation;}Transform Functions
Section titled “Transform Functions”The OT engine handles all 9 operation pairs (3×3 matrix):
import { transform, transformAgainstAll } from '@witqq/spreadsheet-plugins';
// Transform opA against opB → [opA', opB']const [aPrime, bPrime] = transform(opA, opB);
// Transform local op against a list of server opsconst transformed = transformAgainstAll(localOp, serverOps);Transform rules:
| opA × opB | Behavior |
|---|---|
setCellValue × setCellValue | Same cell → last-writer-wins (opB wins); different cells → no conflict |
setCellValue × insertRow | Cell row ≥ insert row → shift down by insert count |
setCellValue × deleteRow | Cell in deleted range → no-op; cell below → shift up |
insertRow × insertRow | Earlier insert shifts later insert down |
insertRow × deleteRow | Adjusts positions based on relative ranges |
deleteRow × deleteRow | Overlapping ranges → compute remaining deletions |
Invariant: apply(apply(state, opA), opB') === apply(apply(state, opB), opA')
OTTransport Interface
Section titled “OTTransport Interface”interface OTTransport { send(op: VersionedOperation): void; onReceive(handler: (op: VersionedOperation) => void): void; onAck(handler: (revision: number) => void): void; disconnect(): void;}WebSocketTransport
Section titled “WebSocketTransport”Connects to the collaboration relay server:
interface WebSocketTransportConfig { url: string; onInit?: (data: { clientId: string; color: string; revision: number; cursors: CursorInfo[] }) => void; onCursor?: (info: CursorInfo) => void; onJoin?: (info: { clientId: string; color: string; name: string }) => void; onLeave?: (info: { clientId: string }) => void;}| Method | Description |
|---|---|
connect() | Establish WebSocket connection (async) |
send(op) | Send operation to server |
sendCursor(cursor) | Send cursor position update |
disconnect() | Close connection and clean up |
getClientId() | Get assigned client ID |
MockTransport
Section titled “MockTransport”For testing — connects two clients directly without a server:
const [transportA, transportB] = MockTransport.createPair();
// Operations sent by A are received by B (and vice versa)// Each transport auto-acks with incrementing revision numbersRemoteCursorLayer
Section titled “RemoteCursorLayer”Canvas render layer that displays colored cell overlays for remote users:
const cursorLayer = new RemoteCursorLayer();
// Set or update a remote cursorcursorLayer.setCursor('client-2', { clientId: 'client-2', color: '#3498db', name: 'Bob', row: 5, col: 3,});
// Remove when user leavescursorLayer.removeCursor('client-2');
// Get all active cursorsconst cursors: RemoteCursor[] = cursorLayer.getCursors();Each cursor renders as:
- Semi-transparent colored fill over the cell (20% opacity)
- 2px solid colored border
- Name label above the cell with colored background
Plugin API
Section titled “Plugin API”| Method | Description |
|---|---|
getPendingCount() | Number of unacknowledged local operations |
getRevision() | Current server revision number |
Collaboration Server
Section titled “Collaboration Server”The relay server (packages/server/) manages operation ordering, transformation, cursor relay, and session lifecycle.
Starting the Server
Section titled “Starting the Server”import { createCollabServer } from '@witqq/spreadsheet-server';
const { wss, close } = createCollabServer(3151);// Server listens on ws://localhost:3151Or via Docker:
npm run dev # Starts both app (3150) and collab server (3151)createCollabServer(port)
Section titled “createCollabServer(port)”Returns { wss: WebSocketServer, close: () => void }.
The server:
- Assigns each connecting client a unique ID and color (rotating through 8 colors)
- Sends
initmessage with client info, current revision, and active cursors - Broadcasts
join/leaveevents to other clients
Server-Side OT Transform
Section titled “Server-Side OT Transform”When the server receives an operation:
- Transforms it against all operations since the client’s revision
- Assigns a new revision number
- Stores in history
- Sends
ackto the sender with the new revision - Broadcasts the transformed operation to all other clients
If transformation results in a no-op, the server still sends an ack.
Cursor Relay
Section titled “Cursor Relay”Cursor updates are relayed to all other clients without transformation. Each cursor message includes clientId, color, name, and cursor position.
Auto-Reset
Section titled “Auto-Reset”When the last client disconnects, the server resets its state:
- Clears operation history
- Resets revision counter to 1
This ensures a clean state for the next session.
Message Types
Section titled “Message Types”| Type | Direction | Fields |
|---|---|---|
init | Server → Client | clientId, color, revision, cursors |
op | Both directions | clientId, revision, op |
ack | Server → Client | revision |
cursor | Both directions | clientId, color, name, cursor |
join | Server → Client | clientId, color, name |
leave | Server → Client | clientId |
See Also
Section titled “See Also”- Streaming Data — live data updates without collaboration
- Event System — events emitted during collaborative edits