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.
| Message | Payload |
|---|---|
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 / pong | keepalive (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.