WebSocket Integration

Add real-time auto-save and multi-user collaboration to RTE with a single script tag.

rte-ws.js is a standalone companion file for RTE that connects to any WebSocket backend. It provides debounced auto-save, real-time content syncing between users, automatic reconnection with exponential backoff, and heartbeat keep-alive.

💾
Auto-Save

Debounced content sync to your backend on every change.

👥
Collaboration

Broadcast and receive changes between multiple users in real time.

🔄
Auto-Reconnect

Exponential backoff reconnection with heartbeat keep-alive.

Installation

Script Tag (CDN)
<script src="https://rte.whitneys.co/rte.js"></script>
<script src="https://rte.whitneys.co/rte-ws.js"></script>
Self-Hosted
<script src="rte.js"></script>
<script src="rte-ws.js"></script>
CommonJS / Node
const RTE = require('rte-rich-text-editor');
const RTEWS = require('./rte-ws.js');
Zero dependencies. rte-ws.js only requires a browser with native WebSocket support (all modern browsers).

Quick Start

<div id="editor"></div>

<script src="https://rte.whitneys.co/rte.js"></script>
<script src="https://rte.whitneys.co/rte-ws.js"></script>
<script>
  const editor = RTE.init('#editor');

  const ws = RTEWS.connect(editor, 'wss://yourserver.com/ws', {
    docId: 'doc-123',
    userId: 'user-abc',
    onOpen: () => console.log('Connected'),
    onSaved: (msg) => console.log('Saved, version:', msg.version),
    onRemoteUpdate: (msg) => console.log('Update from:', msg.userId),
  });
</script>

That's it. The editor will auto-save content to your backend and receive real-time updates from other users.

Configuration

Pass an options object as the third argument to RTEWS.connect():

OptionTypeDefaultDescription
docIdStringnullDocument identifier sent with all messages
userIdStringnullUser identifier for collaboration
debounceMsNumber1000Milliseconds to debounce before sending changes
autoSaveBooleantrueAutomatically send changes on editor input
reconnectBooleantrueAuto-reconnect on disconnect
reconnectBaseMsNumber1000Initial reconnect delay (doubles each attempt)
reconnectMaxMsNumber30000Maximum reconnect delay
heartbeatMsNumber30000Ping interval in ms (0 to disable)
onOpenFunctionnullCalled when WebSocket connects. Receives ws instance.
onCloseFunctionnullCalled when WebSocket closes. Receives close event.
onErrorFunctionnullCalled on WebSocket error or server error message.
onSavedFunctionnullCalled when server confirms a save. Receives message.
onRemoteUpdateFunctionnullCalled when a remote user's change is applied.
onMessageFunctionnullCalled for every incoming message (raw handler).

Message Protocol

All messages are JSON. Your backend should handle these message types:

Outgoing (Client → Server)
TypeFieldsDescription
"join" docId, userId Sent on connect. Server should respond with a "load" message.
"change" docId, userId, html, text, words, chars Sent on editor change (debounced). Server should broadcast to other users and optionally persist.
"save" docId, userId, html, text, words, chars Explicit save request via ws.save(). Server should persist and respond with "saved".
"ping" (none) Heartbeat keep-alive. Server can respond with "pong" or ignore.
Incoming (Server → Client)
TypeFieldsDescription
"load" html Load initial document content into the editor.
"update" html, userId Remote user change. Applied only if userId differs from local user.
"saved" version (optional) Server confirms save. Triggers onSaved callback.
"error" message Server error. Triggers onError callback.

API Reference

RTEWS.connect() returns a connection object with the following methods and properties:

Method / PropertyDescription
ws.save()Send an explicit save request to the server. Returns true if sent.
ws.send(data)Send a custom JSON message. Returns true if sent.
ws.disconnect()Close the connection and stop auto-reconnecting.
ws.reconnect()Manually reconnect (re-enables auto-reconnect).
ws.stateConnection state: "connecting", "open", "closing", or "closed".
ws.socketThe raw WebSocket instance.
Example
const ws = RTEWS.connect(editor, 'wss://api.example.com/ws', {
  docId: 'doc-123',
  userId: 'user-abc',
  debounceMs: 500,
  onSaved: (msg) => console.log('Version:', msg.version),
});

// Explicit save
document.getElementById('save-btn').addEventListener('click', () => {
  ws.save();
});

// Check state
console.log(ws.state); // "open"

// Disconnect when done
ws.disconnect();

Auto-Save

With autoSave: true (the default), every editor change is debounced and sent to the server automatically:

const ws = RTEWS.connect(editor, 'wss://api.example.com/ws', {
  docId: 'my-doc',
  userId: 'user-1',
  debounceMs: 2000,  // wait 2 seconds of inactivity before sending
  onSaved: () => showStatus('Saved'),
});

The server receives a "change" message with the full HTML, plain text, word count, and character count. To disable auto-save and only send on explicit ws.save() calls:

const ws = RTEWS.connect(editor, url, {
  autoSave: false,  // only send when ws.save() is called
});

Real-Time Collaboration

When your server broadcasts "update" messages from one user to others, rte-ws automatically applies the changes while preserving the local user's cursor position.

// User A and User B both connect to the same document
const ws = RTEWS.connect(editor, 'wss://api.example.com/ws', {
  docId: 'shared-doc',
  userId: currentUser.id,
  onRemoteUpdate: (msg) => {
    showNotification(msg.userId + ' made a change');
  },
});
How It Works
  1. User A types — a debounced "change" is sent to the server.
  2. Server broadcasts an "update" with userId: "A" to all other clients.
  3. User B receives the update. Since msg.userId !== cfg.userId, the HTML is applied.
  4. User B's cursor position is saved and restored after the update.
Note: This is full-HTML replacement collaboration. For production use with many concurrent editors, consider implementing operational transforms (OT) or CRDTs on the server to handle conflicts.

Reconnection

rte-ws uses exponential backoff for reconnection:

  • 1st attempt: 1 second
  • 2nd attempt: 2 seconds
  • 3rd attempt: 4 seconds
  • 4th attempt: 8 seconds
  • ...up to reconnectMaxMs (default 30s)

On successful reconnect, a "join" message is sent again so the server can re-associate the client with the document.

const ws = RTEWS.connect(editor, url, {
  reconnect: true,
  reconnectBaseMs: 500,   // start at 500ms
  reconnectMaxMs: 15000,  // cap at 15 seconds
  onOpen: () => console.log('Connected / reconnected'),
  onClose: () => console.log('Disconnected, retrying...'),
});

Backend Examples

Node.js (ws)
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });
const docs = new Map(); // docId -> { html, clients: Set }

wss.on('connection', (socket) => {
  let clientDoc = null;
  let clientUser = null;

  socket.on('message', (raw) => {
    const msg = JSON.parse(raw);

    switch (msg.type) {
      case 'join':
        clientDoc = msg.docId;
        clientUser = msg.userId;
        if (!docs.has(clientDoc)) {
          docs.set(clientDoc, { html: '', clients: new Set() });
        }
        const doc = docs.get(clientDoc);
        doc.clients.add(socket);
        // Send current content
        socket.send(JSON.stringify({ type: 'load', html: doc.html }));
        break;

      case 'change':
        if (clientDoc && docs.has(clientDoc)) {
          const doc = docs.get(clientDoc);
          doc.html = msg.html;
          // Broadcast to other clients
          doc.clients.forEach((client) => {
            if (client !== socket && client.readyState === 1) {
              client.send(JSON.stringify({
                type: 'update',
                html: msg.html,
                userId: msg.userId,
              }));
            }
          });
        }
        break;

      case 'save':
        // Persist to database here...
        socket.send(JSON.stringify({ type: 'saved', version: Date.now() }));
        break;

      case 'ping':
        socket.send(JSON.stringify({ type: 'pong' }));
        break;
    }
  });

  socket.on('close', () => {
    if (clientDoc && docs.has(clientDoc)) {
      docs.get(clientDoc).clients.delete(socket);
    }
  });
});
Python (websockets)
import asyncio, json, websockets

docs = {}  # docId -> { "html": str, "clients": set }

async def handler(ws):
    doc_id = user_id = None

    async for raw in ws:
        msg = json.loads(raw)

        if msg["type"] == "join":
            doc_id = msg["docId"]
            user_id = msg["userId"]
            if doc_id not in docs:
                docs[doc_id] = {"html": "", "clients": set()}
            docs[doc_id]["clients"].add(ws)
            await ws.send(json.dumps({"type": "load", "html": docs[doc_id]["html"]}))

        elif msg["type"] == "change" and doc_id:
            docs[doc_id]["html"] = msg["html"]
            for client in docs[doc_id]["clients"]:
                if client != ws:
                    await client.send(json.dumps({
                        "type": "update", "html": msg["html"], "userId": user_id
                    }))

        elif msg["type"] == "save":
            # Persist to database here...
            await ws.send(json.dumps({"type": "saved"}))

    if doc_id and doc_id in docs:
        docs[doc_id]["clients"].discard(ws)

asyncio.run(websockets.serve(handler, "0.0.0.0", 8080))