Quick start
Install and configure Collab
This quick start provides a summary of a Pitter Patter Collab configuration. For a more in depth explanation, see the Collab Guide. You can also see a fully functional demo in our Github repo.
Configure your backend
npm i @pitter-patter/collab-serverTo set up your backend with Pitter Patter collab, you first create a CollabAuthority. The code below assumes that you have
- a database with support for ACID transactions
- a Redis cluster accessible at
REDIS_URL - a prosemirror editor schema,
schema.
import {
CollabAuthority,
RedisBroadcastManager,
TooMuchContentionError
} from "@pitter-patter/collab-server";
const collabAuthority = new CollabAuthority<Transaction<DB>>(
{
runWithTransaction: async (callback) => {
// start a transaction on your database and execute `callback` within it
},
getDoc: async (tr, docId) => {
// Get the prosemirror document by docId from your database using the
// transaction `tr` if provided.
},
getCommit: async (tr, docId, commitRef) => {
// Get the commit with the given docId and commitRef from your database
// using the transaction `tr` if provided.
},
getCommits: async (tr, docId, version) => {
// Get all commit with the given docId whose version is strictly greater
// than `version` from your database using the transaction `tr` if provided.
},
saveDoc: async (tr, docId, docJSON, version) => {
// Update the document state and version for `docId` in your databse using
// the transaction `tr` if provided.
},
saveCommit: async (tr, docId, commitRef, commitVersion, commitSteps) => {
// Insert the commit for `docId` in your databse using the transaction `tr`
// if provided.
},
broadcastManager: new RedisBroadcastManager({
redisUrl: process.env["REDIS_URL"],
}),
schema,
}Next, create two endpoints. One for clients to submit commits on a document and one for clients to listen for commits on a document.
// Endpoint that clients will use to send commits to the CollabAuthority
app.post("/api/docs/:docId/commits", async (req, res) => {
try {
await collabAuthority.receiveCommit(req.params.docId, req.body);
} catch (e) {
if (e instanceof TooMuchContentionError) {
res.status(409).send(null);
return;
}
throw e;
}
res.status(204).send(null);
});
// Endpoint that clients will use to listen for commits from the CollabAuthority
app.get("/api/docs/:docId/commits", async (req, res) => {
const commits = await collabAuthority.listenForCommit(
req.params.docId,
parseInt(req.query["version"] as string, 10),
);
res.status(200).send(commits);
});Configure your frontend
npm i @pitter-patter/collab-clientTo configure your frontend editor with Collab, you first add the collab plugin when creating your editor state
import { EditorState } from "prosemirror-state";
import { collab } from "@pitter-patter/collab-client";
const document = await fetch(`/api/docs/${docId}`).then((r) => r.json())
const state = EditorState.create({
doc: Node.fromJSON(schema, document.content)
plugins: [collab({ version: document.version })],
})Then create a CollabClient. This client uses the endpoints you created above to send and listen for commits.
// The LongPollListener listens for document changes at the GET
// endpoint `/api/docs/:docId/commits` you created above
const longPollListner = new LongPollListener(
new URL(`/api/docs/${doc.id}/commits`, "http://localhost:3000"),
);
const collabClient = new CollabClient({
sendCommit: async (commit) => {
// send `commit` to the POST endpoint `/api/docs/:docId/commits` you created above
},
receiveCommits: (commits) => {
// merge a new set of commits into your editor state for example:
// const newEditorState = commits.reduce((acc, commit) => acc.apply(receiveCommitTransaction(acc, commit)), oldEditorState),
},
listner: commitListener,
});Tell the client to start listening for changes
const abortController = new AbortController();
collabClient.listen(initialState, abortController.signal).catch((e) => console.error(e));When the local editor state changes, you need to tell the CollabClient to create and send a new commit.
collabClient.send(state).catch((e) => console.error(e));