Collaboration
Seed, edit, and export live Yjs rooms that browsers are editing.
Programmatic access uses revise/server from the revise package and follows the SDK license.
Every Revise document is a Yjs document. A sync server
that relays a room's updates can also seed the room from a file, edit it, and
export it, while people edit the same room in
the revise editor. Use the same version
of both; revise/server also works with rooms that editor 2.3 clients
(@reviseio/editor@2.3) edit.
Seeding a room#
seedYDoc(ydoc, source) writes a document into an empty room and returns
whether it wrote anything. A room that already holds a document is left
alone, so call it on every connection. Two peers that both seeded an empty
room before syncing are repaired to one copy.
With Hocuspocus:
import { Server } from "@hocuspocus/server";
import { migrateDocument, seedYDoc } from "revise/server";
const server = new Server({
port: 1234,
async onLoadDocument({ document, documentName }) {
// Load stored state first if you persist rooms, then:
migrateDocument(document);
await seedYDoc(document, `documents/${documentName}.docx`);
},
});
await server.listen();Clients join with seed: "never" and no source file, so only the server ever
seeds. migrateDocument moves table proposals stored by versions before 4.2
into the document's review store, once: browser editors in a host-synced room
never migrate it themselves. It is idempotent, so call it on every load
(under a lock if several server processes can load one document at once). A Word file is stored in the room along with its content, so any
session over the room, on the server or in the editor, exports untouched
content byte for byte.
Editing a live room#
import { ReviseDocument } from "revise/server";
import type * as Y from "yjs";
async function reviewRoom(room: Y.Doc): Promise<string[]> {
const doc = await ReviseDocument.fromYDoc(room, { author: "Review bot" });
try {
const [hit] = doc.grep("Delaware");
if (!hit) return [];
const edit = await doc.tools.call("replace", {
id: hit.id,
replacements: [{ find: "Delaware", replace: "New York" }],
});
return edit.ok ? (edit.value.suggestionIds ?? []) : [];
} finally {
doc.dispose(); // the room stays alive
}
}Edits are tracked changes by default, so they appear in every open editor as suggestions for a person to accept or reject. Peers receive them as ordinary Yjs updates, and their carets stay in place.
Exporting a live room#
import { ReviseDocument } from "revise/server";
import type * as Y from "yjs";
async function downloadRoom(room: Y.Doc): Promise<Uint8Array> {
const doc = await ReviseDocument.fromYDoc(room);
try {
const { bytes } = await doc.export({ format: "docx" });
return bytes;
} finally {
doc.dispose();
}
}fromYDoc never replaces or destroys your Y.Doc: dispose() releases only
the tool session, and reload() is not available on a room.
Without a live transport#
A request handler can take a client's document as one encoded update, edit it, and return only the delta, which merges into the live document like any peer's edit:
import * as Y from "yjs";
import { ReviseDocument } from "revise/server";
async function review(update: Uint8Array): Promise<Uint8Array> {
const doc = await ReviseDocument.restore(update);
try {
const baseline = Y.encodeStateVector(doc.ydoc);
await doc.replaceAll("Delaware", "New York");
return Y.encodeStateAsUpdate(doc.ydoc, baseline);
} finally {
doc.dispose();
}
}