@trupdf/collaboration
WebSocket real-time sync. Exponential reconnect, presence, awaitable acks, stop-on-destroy.
CollaborationEngine
import { CollaborationEngine } from '@trupdf/viewer';
const collab = new CollaborationEngine({
url: 'wss://collab.example.com/v1',
documentId: 'doc-123',
name: 'Alice',
authToken: sessionToken, // optional; sent in the join message
});
collab.connect();Options
| Option | Default | Purpose |
|---|---|---|
url | — | WebSocket URL (required) |
documentId | — | Room to join (required) |
name | — | Display name for cursor / badge (required) |
authToken | — | Bearer token validated server-side |
initialBackoffMs | 500 | First reconnect delay |
maxBackoffMs | 30000 | Reconnect delay cap |
heartbeatMs | 15000 | Ping interval |
websocketCtor | global | Injectable WebSocket constructor (tests) |
Methods
| Method | Purpose |
|---|---|
connect() | Open the socket and send the join message |
send(msg) | Send any wire message (protocol field added automatically) |
sendOp(op) | Send an annotation operation; resolves with the server-assigned seq on ack |
getPeers() | Snapshot of remote peers (PresenceState[]) |
getPeerId() | This client's server-assigned peer id (null before join) |
destroy() | Close the socket, stop reconnect + heartbeat timers, clear listeners |
Events
collab.on('open', () => /* joined */);
collab.on('close', ({ code, reason, clean }) => /* … */);
collab.on('message', (msg) => /* any wire message */);
collab.on('presence', (peers) => /* readonly PresenceState[] */);
collab.on('error', (err) => /* … */);Wire protocol
COLLAB_PROTOCOL_VERSION = 1. Every message carries a protocol
field. Message types:
| Type | Direction | Purpose |
|---|---|---|
join | client → server | { documentId, name, token? } |
joined | server → client | { peerId, peers[], lastSeq } — room snapshot |
leave | server → client | { peerId } |
presence | both | { peerId, page, cursor?, selection? } — cursor in PDF user space |
op | both | { peerId, seq, op } — op.kind: 'add' | 'update' | 'remove' |
ack | server → client | { requestId, seq } — resolves the matching sendOp promise |
error | server → client | { code, message, requestId? } |
ping / pong | client ⇄ server | Keepalive |
Typed message interfaces (JoinMessage, JoinedMessage,
PresenceMessage, OperationMessage, AckMessage, ErrorMessage,
…) are exported from the package. Bring your own server — any
WebSocket backend that speaks this protocol works.
Reconnect
Exponential backoff from initialBackoffMs doubling to the
maxBackoffMs cap. Heartbeat ping every 15 s detects dead links
faster than TCP. Stops on destroy — no zombie sockets.
License gate
The constructor calls gate.require('collab'). Trial cannot connect.