TPTruPDF

Real-time collaboration

WebSocket-backed annotation sync with presence, optimistic ops, exponential reconnect, and stop-on-destroy.

Connect

import { CollaborationEngine } from '@trupdf/viewer';
 
const collab = new CollaborationEngine({
  url: 'wss://collab.example.com/v1',
  documentId: 'doc-123',
  name: 'Alice',
  authToken: sessionToken, // optional; validated server-side
});
 
collab.connect(); // opens the socket and joins the room
collab.on('open', () => console.log('connected'));

The server assigns a stable peer id on join — read it with collab.getPeerId() (returns null until the joined message arrives).

Presence

collab.on('presence', (peers) => {
  // peers: { peerId, name, color, page?, cursor?, selection?, lastSeenAt }[]
  renderAvatars(peers);
});
 
// Broadcast your own cursor / selection:
collab.send({
  type: 'presence',
  peerId: collab.getPeerId()!,
  page: 3,
  cursor: { x: 100, y: 200 }, // PDF user space
  selection: annotationId, // single-select v1
});

getPeers() returns the current roster synchronously.

Operations

Annotation mutations go through sendOp, which returns a promise resolving with the server-assigned sequence number once the server acks — or rejecting if the connection drops first:

const seq = await collab.sendOp({
  type: 'op',
  op: { kind: 'add', record }, // or { kind: 'update', id, patch } / { kind: 'remove', id }
});

Wire protocol

Every message is stamped with protocol: COLLAB_PROTOCOL_VERSION (currently 1); mismatched versions are rejected.

MessagePayload
join{ documentId, name, token? }
joined{ peerId, peers[], lastSeq }
leave{ peerId }
presence{ peerId, page, cursor?, selection? }
op{ peerId, seq, op }
ack{ requestId, seq }
error{ code, message, requestId? }
ping / pongkeepalive (client pings, server replies)

Engine events: open, close, message (every parsed wire message), presence, error.

Reconnect

Exponential backoff starting at 500 ms, doubling to a 30 s cap (tune via initialBackoffMs / maxBackoffMs). Heartbeat ping every 15 s (heartbeatMs) detects dead links faster than TCP. Operations awaiting an ack when the connection drops are rejected so callers can retry. Stops on destroy — no zombie sockets.

Disconnect

collab.destroy();

Always paired with the host unmount. destroy() clears the heartbeat interval and any pending reconnect timer, rejects pending acks, and closes the socket.

License gating

CollaborationEngine calls gate.require('collab') in its constructor. Trial tokens cannot connect.

Server reference

TruPDF doesn't ship a hosted collaboration server. Bring your own — the wire protocol is small, versioned, and fully described by the table above (typed message shapes ship with the package). Reference server implementations are tracked in the public roadmap.