Skip to content

Latest commit

 

History

History

websocket-prosemirror-blocks

WebSocket-Prosemirror-Blocks

A basic collaborative rich-text editor using list-positions and @list-positions/formatting, a WebSocket server, and ProseMirror. It supports only a simple block-based schema, stored in a "list-positions-native" way.

When a client makes a change, a description is sent to the server in JSON format. The server echoes that change to all other connected clients. The server also updates its own copy of the rich-text state; this is sent to new clients when they load the page.

A client optimistically updates its own state before sending its change to the server. The demo is simple enough that these optimistic updates are always "correct" (match the server's eventual state) - see the comments in src/server/rich_text_server.ts. A more complicated app might need to "repair" optimistic updates that are rejected or modified by the server, e.g., due to permissions issues.

The collaborative state is linear, not a tree like ProseMirror's own state; it is stored as Lists in ProsemirrorWrapper. The collaborative state uses special "block markers" to indicate the start of each block and its type (paragraph, h1, h2, ul, ol). In particular, bullets and numbering are stored as a series of unordered or ordered list blocks, with no explicit list start/end; at render time, we fill in numbers and render the numbers/bullets using a CSS ::before element on a normal paragraph.

The ProseMirror wrapper uses its copy of the collaborative state as the source-of-truth for ProseMirror. Whenever that state changes, ProsemirrorWrapper.sync() recomputes the ProseMirror state and sends it to ProseMirror. When ProseMirror generates a transaction due to a local change (e.g. typing), ProsemirrorWrapper.onLocalTr converts that transaction into changes to the collaborative state, then updates the server and calls ProsemirrorWrapper.sync(). Calling sync() is technically redundant, but it ensures that the two states don't diverge, and it makes the data flow consistent between local vs remote changes.

References: unpublished notes by Martin Kleppmann (2022); Notion's data model; y-prosemirror

Code organization:

  • src/common/: Messages shared between clients and the server.
  • src/server/: WebSocket server.
  • src/site/: ProseMirror client.

Installation

First, install Node.js. Then run npm i.

Commands

npm run dev

Build the app from src/, in development mode.

npm run build

Build the app from src/, in production mode.

npm start

Run the server on http://localhost:3000/. Use multiple browser windows at once to test collaboration.

To change the port, set the $PORT environment variable.

npm run clean

Delete dist/.