[]
This tutorial guides developers through the server-side getOps and fetchHistorySnapshot APIs to implement history management. You will:
View the list of operations
Preview specific versions
The history will be displayed in a sidebar on the right side of the editor. While previewing, the editor will disable editing. Editing capabilities will be restored after exiting the preview.
Complete the Real-Time Collaborative Text Editor tutorial.
Enter the string "Hello!" in the input field and click the Refresh button. The right sidebar will display the operation history list, which records all operations. Click the Preview button for a specific version to view the corresponding editor state in the input field.

Server-Side Extensions:
Add a /history/:roomId endpoint, using the getOps method to retrieve operation history.
Add a /snapshot/:roomId/:version endpoint, using the fetchHistorySnapshot method to retrieve historical version snapshots.
Client-Side Enhancements:
Add a sidebar on the right to display the history.
Implement the "Preview" functionality to load snapshots and disable editing.
Add the "Exit Preview" functionality to restore the current document and editing capabilities.
Modify server.js to support the history API.
import express from "express";
import { createServer } from "http";
import { Server } from "@mescius/js-collaboration";
import OT from "@mescius/js-collaboration-ot";
import richText from "rich-text";
const app = express();
const httpServer = createServer(app);
const server = new Server({ httpServer });
// Register rich-text type
OT.TypesManager.register(richText.type);
// Initialize OT document services
const documentServices = new OT.DocumentServices();
server.useFeature(OT.documentFeature(documentServices));
// Serve static files
app.use(express.static("public"));
// Get operation history
app.get("/history/:roomId", async (req, res) => {
const roomId = req.params.roomId;
try {
const ops = await documentServices.getOps(roomId, 0);
res.json({ operations: ops });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Get snapshot for a specific version
app.get("/snapshot/:roomId/:version", async (req, res) => {
const roomId = req.params.roomId;
const version = parseInt(req.params.version, 10);
try {
const snapshot = await documentServices.fetchHistorySnapshot(roomId, version);
res.json(snapshot);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Start the server
httpServer.listen(8080, () => {
console.log("Server running at http://localhost:8080");
});Code Explanation
The bolded sections are the newly added code, introducing two REST endpoints:
/history/:roomId: Returns the full operation history of a room.
/snapshot/:roomId/:version: Returns the document state at a specific version.
Modify files index.html, styles.css, and client.js to add functionalities of history viewing and preview.
Update public/index.html
Replace the HTML from the basic tutorial with the following content:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Real-Time Collaborative Text Editor with History</title>
<link href="https://cdn.quilljs.com/1.3.6/quill.bubble.css" rel="stylesheet">
<link rel="stylesheet" href="./styles.css">
<script src="./client.bundle.js" defer></script>
</head>
<body>
<div class="container">
<h1>Real-Time Collaborative Text Editor with History</h1>
<div class="main-content">
<div class="editor-area">
<div id="editor"></div>
<div id="preview-editor" style="display: none;"></div>
</div>
<div class="sidebar">
<h3>History</h3>
<div class="history-controls">
<button onclick="loadHistory()">Refresh</button>
<button id="exit-preview" onclick="exitPreview()" style="display: none;">Exit Preview</button>
</div>
<ul id="history-list"></ul>
</div>
</div>
</div>
</body>
</html>Update public/client.js
Replace the code from the basic tutorial with the following content:
import { Client } from "@mescius/js-collaboration-client";
import * as OT from "@mescius/js-collaboration-ot-client";
import richText from "rich-text";
import Quill from "quill";
// Register rich-text type
OT.TypesManager.register(richText.type);
// Connect to server and join room
const connection = new Client().connect("room-id");
const doc = new OT.SharedDoc(connection);
const quill = new Quill("#editor", { theme: "bubble" });
const previewQuill = new Quill("#preview-editor", { theme: "bubble", readOnly: true });
// Subscribe to document
doc.subscribe().then(async () => {
if (!doc.type) {
try {
await doc.create([{ insert: "Hi!" }], richText.type.uri, {});
} catch (err) {
if (err) console.error("Error: " + err); // Fixed: Pass single error object
}
}
quill.setContents(doc.data);
quill.on("text-change", (delta, oldDelta, source) => {
if (source !== "user") return;
doc.submitOp(delta, { source: connection.id });
});
doc.on("op", (op, source) => {
if (source === connection.id) return;
quill.updateContents(op);
});
loadHistory(); // Load history on start
})
// Error handling (fixed: consolidated and corrected)
connection.on("error", (err) => console.error("Connection error:", err.message));
doc.on("error", (err) => console.error("Document error:", err.message));
// Load history
async function loadHistory() {
const response = await fetch("/history/room-id");
const data = await response.json();
const historyList = document.getElementById("history-list");
historyList.innerHTML = "";
data.operations.forEach((op, index) => {
const li = document.createElement("li");
const infoDiv = document.createElement("div");
infoDiv.classList.add("info");
infoDiv.textContent = `Version ${index}: ${JSON.stringify(op.create || op.op)}`;
const previewBtn = document.createElement("button");
previewBtn.textContent = "Preview";
previewBtn.addEventListener("click", () => previewVersion(index + 1));
li.appendChild(infoDiv);
li.appendChild(previewBtn);
historyList.appendChild(li);
});
}
// Preview a specific version
async function previewVersion(version) {
const response = await fetch(`/snapshot/room-id/${version}`);
const snapshot = await response.json();
document.getElementById("editor").style.display = "none";
document.getElementById("preview-editor").style.display = "block";
previewQuill.setContents(snapshot.data);
document.getElementById("exit-preview").style.display = "inline-block";
}
// Exit preview mode
function exitPreview() {
document.getElementById("editor").style.display = "block";
document.getElementById("preview-editor").style.display = "none";
previewQuill.setContents([]);
document.getElementById("exit-preview").style.display = "none";
}
// Global functions
window.loadHistory = loadHistory;
window.exitPreview = exitPreview;Update public/styles.css
html,
body {
height: 100%;
margin: 0;
font-family: Arial, sans-serif;
background: #f0f2f5;
}
.container {
height: 100vh;
display: flex;
flex-direction: column;
background: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
padding: 15px;
margin: 0;
color: #333;
text-align: center;
border-bottom: 1px solid #ddd;
}
.main-content {
display: flex;
flex: 1;
padding: 15px;
}
.editor-area {
flex: 2;
display: flex;
flex-direction: column;
}
#editor,
#preview-editor {
height: 100%;
border: 1px solid #ccc;
font-size: large;
}
.sidebar {
flex: 1;
background: #f5f5f5;
border: 1px solid #ddd;
padding: 15px;
display: flex;
flex-direction: column;
}
.sidebar h3 {
margin: 0 0 10px 0;
font-size: 16px;
color: #333;
}
.history-controls {
margin-bottom: 15px;
}
#history-list {
list-style: none;
padding: 0;
overflow-y: auto;
flex: 1;
}
#history-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
transition: background 0.2s;
}
#history-list li:hover {
background: #f0f0f0;
}
.info {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 10px;
font-size: 14px;
max-width: 400px;
}
button {
padding: 8px 16px;
background: #007bff;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #0056b3;
}
#exit-preview {
margin-left: 10px;
}Build the Client Code
npm run buildStart the Server
npm run startTest the Functionality
Open http://127.0.0.1:8080/index.html.
Edit the document and click "Refresh" to view the history on the right.
Click "Preview" to view a historical version (editing disabled), then click "Exit Preview" to restore.